From 20a01a566bfe13b9edc355b18c4af3d44bf82715 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Tue, 9 Apr 2019 13:00:09 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E5=88=86=E6=94=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/vcs.xml | 6 ++++++ .../main/java/com/example/main/init/ui/SplashActivity.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt index d88a4d3..1714dbf 100644 --- a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt +++ b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt @@ -3,7 +3,7 @@ package com.example.main.init.ui /** * Anthor: Zhuangmingzhu * Date: 2019/4/8 上午11:23 - * Describe: + * Describe:闪屏Activity */ class SplashActivity { } \ No newline at end of file From c106c1be4f6f6edbbf26cb6fe3ad3a7420ab978e Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Thu, 11 Apr 2019 20:03:06 +0800 Subject: [PATCH 02/12] =?UTF-8?q?BaseActivity=E5=92=8C=E9=97=AA=E5=B1=8F?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/build.gradle | 14 +- .../java/com/example/core/model/Version.kt | 18 +++ .../com/example/core/util/AndroidVersion.kt | 52 +++++++ .../java/com/example/core/util/GlobalUtil.kt | 49 ++++++ .../com/example/main/feeds/ui/MainActivity.kt | 24 +++ .../example/main/init/ui/SplashActivity.kt | 70 ++++++++- .../com/example/main/login/ui/AuthActivity.kt | 42 ++++++ .../example/main/login/ui/LoginActivity.kt | 62 ++++++++ .../java/com/example/main/ui/BaseActivity.kt | 142 ++++++++++++++++++ main/src/main/res/layout/bad_network_view.xml | 34 +++++ main/src/main/res/layout/load_error_view.xml | 33 ++++ main/src/main/res/layout/no_content_view.xml | 30 ++++ main/src/main/res/layout/view_stub_holder.xml | 22 +++ .../main/res/values-v21/transition_svg.xml | 5 + 14 files changed, 590 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/com/example/core/model/Version.kt create mode 100644 core/src/main/java/com/example/core/util/AndroidVersion.kt create mode 100644 core/src/main/java/com/example/core/util/GlobalUtil.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/MainActivity.kt create mode 100644 main/src/main/java/com/example/main/login/ui/AuthActivity.kt create mode 100644 main/src/main/java/com/example/main/login/ui/LoginActivity.kt create mode 100644 main/src/main/res/layout/bad_network_view.xml create mode 100644 main/src/main/res/layout/load_error_view.xml create mode 100644 main/src/main/res/layout/no_content_view.xml create mode 100644 main/src/main/res/layout/view_stub_holder.xml create mode 100644 main/src/main/res/values-v21/transition_svg.xml diff --git a/core/build.gradle b/core/build.gradle index be16efc..822a6e8 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,16 +3,13 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion COMPILE_SDK_VERSION as int defaultConfig { - minSdkVersion 15 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" - + minSdkVersion MIN_SDK_VERSION as int + targetSdkVersion TARGET_SDK_VERSION as int testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -26,6 +23,10 @@ android { } +androidExtensions { + experimental = true +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) @@ -35,6 +36,7 @@ dependencies { androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" api 'org.litepal.android:kotlin:2.1.0' + api 'com.google.code.gson:gson:2.8.5' } repositories { mavenCentral() diff --git a/core/src/main/java/com/example/core/model/Version.kt b/core/src/main/java/com/example/core/model/Version.kt new file mode 100644 index 0000000..bd57c0b --- /dev/null +++ b/core/src/main/java/com/example/core/model/Version.kt @@ -0,0 +1,18 @@ +package com.example.core.model + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午4:59 + * Describe:版本更新的实体类封装,如果存在版本更新则会提供下面描述信息 + */ +@Parcelize +class Version(@SerializedName("change_log") val changeLog:String, + @SerializedName("is_force") val isForce: Boolean, + val url: String, + @SerializedName("version_name") val versionName: String, + @SerializedName("version_code") val versionCode: Int, + val channel: String):Parcelable \ No newline at end of file diff --git a/core/src/main/java/com/example/core/util/AndroidVersion.kt b/core/src/main/java/com/example/core/util/AndroidVersion.kt new file mode 100644 index 0000000..f5286b8 --- /dev/null +++ b/core/src/main/java/com/example/core/util/AndroidVersion.kt @@ -0,0 +1,52 @@ +package com.example.core.util + +import android.os.Build + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午2:45 + * Describe:以更加可读的方式提供Android系统版本号的判断方法 + */ +object AndroidVersion { + + //判断当前手机系统版本API是否是16以上 + fun hasJellyBean():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN + } + + //判断当前手机系统版本API是否是17以上 + fun hasJellyBeanMR1():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR1 + } + + //判断当前手机系统版本API是否是18以上 + fun hasJellyBeanMR2():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN_MR2 + } + + //判断当前手机系统版本API是否是19以上 + fun hasKitkat():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT + } + + //判断当前手机系统版本API是否是21以上 + fun hasLollipop():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP + } + + //判断当前手机系统版本API是否是22以上 + fun hasLollipopMR1():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP_MR1 + } + + //判断当前手机系统版本API是否是23以上 + fun hasMarshmallow():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.M + } + + //判断当前手机系统版本API是否是24以上 + fun hasNougat():Boolean{ + return Build.VERSION.SDK_INT>=Build.VERSION_CODES.N + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/util/GlobalUtil.kt b/core/src/main/java/com/example/core/util/GlobalUtil.kt new file mode 100644 index 0000000..db12a1a --- /dev/null +++ b/core/src/main/java/com/example/core/util/GlobalUtil.kt @@ -0,0 +1,49 @@ +package com.example.core.util + +import android.widget.Toast +import com.example.core.GifFun +import java.text.SimpleDateFormat +import java.util.* + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午4:31 + * Describe:应用程序全局的通用工具类,功能比较单一,经常被复用的功能,应该封装到此工具类中,从而给全局代码提供方面的操作 + */ +object GlobalUtil { + private val TAG = "GlobalUtil" + + private var toast: Toast? = null + + //获取当前应用程序的包名 + val appPackage: String + get() = GifFun.getContext().packageName + + //获取当前应用程序的名称 + val appName: String + get() = GifFun.getContext().resources.getString(GifFun.getContext().applicationInfo.labelRes) + + //获取当前应用程序的版本名 + val appVersionName: String + get() = GifFun.getContext().packageManager.getPackageInfo(appPackage, 0).versionName + + //获取当前应用程序的版本号 + val appVersionCode: Int + get() = GifFun.getContext().packageManager.getPackageInfo(appPackage, 0).versionCode + + //获取当前时间的字符串,格式为yyyyMMddHHmmss + val currentDateString: String + get() { + val sdf=SimpleDateFormat("yyyyMMddHHmmss", Locale.US) + return sdf.format(Date()) + } + + //将当前线程睡眠指定毫秒数 + fun sleep(millis:Long){ + try { + Thread.sleep(millis) + }catch (e:InterruptedException){ + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt b/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt new file mode 100644 index 0000000..0f9b04c --- /dev/null +++ b/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt @@ -0,0 +1,24 @@ +package com.example.main.feeds.ui + +import android.app.Activity +import android.content.Intent + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午6:27 + * Describe: + */ +class MainActivity { + + companion object { + + private const val TAG="MainActivity" + + private const val REQUEST_SEARCH=10000 + + fun actionStart(activity: Activity){ + val intent= Intent(activity,MainActivity::class.java) + activity.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt index 1714dbf..e67a2e6 100644 --- a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt +++ b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt @@ -1,9 +1,77 @@ package com.example.main.init.ui +import android.os.Bundle +import android.view.View +import com.example.core.GifFun +import com.example.core.model.Version +import com.example.core.util.GlobalUtil +import com.example.main.feeds.ui.MainActivity +import com.example.main.login.ui.LoginActivity +import com.example.main.ui.BaseActivity + /** * Anthor: Zhuangmingzhu * Date: 2019/4/8 上午11:23 * Describe:闪屏Activity */ -class SplashActivity { +abstract class SplashActivity :BaseActivity(){ + + //记录进入SplashActivity的时间 + var enterTime:Long=0 + + //判断是否正在跳转或者已经跳转到下一个界面 + var isForwarding=false + + var hasNewVersion=false + + lateinit var logoView: View + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTime=System.currentTimeMillis() + + } + + //设置闪屏界面的最大延迟跳转,让用户不至于在闪屏界面等太久 + private fun delayToForward(){ + Thread(Runnable { + GlobalUtil.sleep(MAX_WAIT_TIME.toLong()) + forwardToNextActivity(false,null) + }).start() + } + + //跳转到下一个Activity.如果闪屏界面停留的时间还不足最短时间,要等待一会,保证闪屏不会一闪而过 + open fun forwardToNextActivity(hasNewVersion:Boolean,version: Version?){ + if(!isForwarding){ + isForwarding=true + val currentTime=System.currentTimeMillis() + val timeSpent=currentTime-enterTime + if(timeSpent< MIN_WAIT_TIME){ + GlobalUtil.sleep(MIN_WAIT_TIME-timeSpent) + } + runOnUiThread{ + if(GifFun.isLogin()){ + MainActivity.actionStart(this) + finish() + }else{ + if(isActive){ + LoginActivity.actionStartWithTransition(this,logoView,hasNewVersion,version) + }else{ + LoginActivity.actionStart(this,hasNewVersion,version) + finish() + } + } + } + } + } + + companion object { + private const val TAG="SplashActivity" + + //闪屏的最短时间 + const val MIN_WAIT_TIME=2000 + + //闪屏的最长时间 + const val MAX_WAIT_TIME=5000 + } } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/login/ui/AuthActivity.kt b/main/src/main/java/com/example/main/login/ui/AuthActivity.kt new file mode 100644 index 0000000..a99defe --- /dev/null +++ b/main/src/main/java/com/example/main/login/ui/AuthActivity.kt @@ -0,0 +1,42 @@ +package com.example.main.login.ui + +import com.example.main.ui.BaseActivity + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午6:49 + * Describe:登录和注册的基类,用于封装登录和注册时通用的逻辑 + */ +abstract class AuthActivity :BaseActivity(){ + companion object { + + private const val TAG="AuthActivity" + + /** + * QQ第三方登录的类型。 + */ + const val TYPE_QQ_LOGIN = 1 + + /** + * 微信第三方登录的类型。 + */ + const val TYPE_WECHAT_LOGIN = 2 + + /** + * 微博第三方登录的类型。 + */ + const val TYPE_WEIBO_LOGIN = 3 + + /** + * 手机号登录的类型。 + */ + const val TYPE_PHONE_LOGIN = 4 + + /** + * 游客登录的类型,此登录只在测试环境下有效,线上环境没有此项功能。 + */ + const val TYPE_GUEST_LOGIN = -1 + } + + protected abstract fun forwardToMainActivity() +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/login/ui/LoginActivity.kt b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt new file mode 100644 index 0000000..5426040 --- /dev/null +++ b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt @@ -0,0 +1,62 @@ +package com.example.main.login.ui + +import android.app.Activity +import android.app.ActivityOptions +import android.content.Intent +import android.view.View +import com.example.core.GifFun +import com.example.core.model.Version +import com.example.core.util.AndroidVersion +import com.example.main.R + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/11 下午6:48 + * Describe:登录界面 + */ +abstract class LoginActivity :AuthActivity() { + + companion object { + private const val TAG="LoginActivity" + + @JvmStatic val START_WITH_TRANSITION="start_with_transition" + + @JvmStatic val INTENT_HAS_NEW_VERSION="intent_has_new_version" + + @JvmStatic val INTENT_VERSION = "intent_version" + + private val ACTION_LOGIN="${GifFun.getPackageName()}.ACTION_LOGIN" + + private val ACTION_LOGIN_WITH_TRANSITION = "${GifFun.getPackageName()}.ACTION_LOGIN_WITH_TRANSITION" + + //启动LoginActivity + fun actionStart(activity: Activity,hasNewVersion:Boolean,version:Version?){ + val intent=Intent(ACTION_LOGIN).apply { + putExtra(INTENT_HAS_NEW_VERSION,hasNewVersion) + putExtra(INTENT_VERSION,version) + } + activity.startActivity(intent) + } + + //启动LoginActivity并附带Transition动画 + fun actionStartWithTransition(activity: Activity,logo: View,hasNewVersion:Boolean,version:Version?){ + val intent=Intent(ACTION_LOGIN_WITH_TRANSITION).apply { + putExtra(INTENT_HAS_NEW_VERSION,hasNewVersion) + putExtra(INTENT_VERSION,version) + } + if(AndroidVersion.hasLollipop()){ + intent.putExtra(START_WITH_TRANSITION,true) + val options=ActivityOptions.makeSceneTransitionAnimation(activity,logo,activity.getString(R.string.transition_logo_splash)) + activity.startActivity(intent,options.toBundle()) + }else{ + activity.startActivity(intent) + activity.finish() + } + } + } + + //是否进行动画 + protected var isTransitioning=false + + +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/ui/BaseActivity.kt b/main/src/main/java/com/example/main/ui/BaseActivity.kt index eff627f..2f5eb87 100644 --- a/main/src/main/java/com/example/main/ui/BaseActivity.kt +++ b/main/src/main/java/com/example/main/ui/BaseActivity.kt @@ -2,11 +2,23 @@ package com.example.main.ui import android.app.Activity import android.app.ProgressDialog +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Color import android.os.Bundle +import android.support.annotation.CallSuper +import android.support.v4.app.ActivityCompat +import android.support.v4.content.ContextCompat import android.support.v7.app.AppCompatActivity import android.support.v7.widget.Toolbar import android.view.View +import android.view.ViewStub +import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.TextView +import com.example.core.util.AndroidVersion import com.example.main.R import com.example.main.common.callback.PermissionListener import com.example.main.util.ActivityCollector @@ -91,5 +103,135 @@ open class BaseActivity :AppCompatActivity() { actionBar?.setDisplayHomeAsUpEnabled(true) } + //将状态栏设置成透明,只适配5.0以上手机 + protected fun transparentStatusBar(){ + if(AndroidVersion.hasLollipop()){ + val decorView=window.decorView + decorView.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + window.statusBarColor=Color.TRANSPARENT + } + } + + //检查和处理运行时权限,并将用户授权的结果通过PermissionListener进行回调 + protected fun handlePermissions(permissions:Array?,listener: PermissionListener){ + if(permissions==null||activity==null){ + return + } + mListener=listener + val requestPermissionList=ArrayList() + for(permission in permissions){ + if(ContextCompat.checkSelfPermission(activity!!,permission)!=PackageManager.PERMISSION_GRANTED){ + requestPermissionList.add(permission) + } + } + if(!requestPermissionList.isEmpty()){ + ActivityCompat.requestPermissions(activity!!,requestPermissionList.toTypedArray(),1) + }else{ + listener.onGranted() + } + } + + //隐藏软键盘 + fun hideSoftKeyboard(){ + val view=currentFocus + if(view!=null) { + val binder = view.windowToken + val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + manager.hideSoftInputFromWindow(binder,InputMethodManager.HIDE_NOT_ALWAYS) + } + } + + //显示软键盘 + fun showSoftKeyboard(editText:EditText?){ + if(editText!=null) { + editText.requestFocus() + val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + manager.showSoftInput(editText, 0) + } + } + + //当服务器返回加载内容失败时,显示该界面 + protected fun showLoadErrorView(tip:String){ + if(loadErrorView!=null){ + loadErrorView?.visibility=View.VISIBLE + return + } + val viewStub=findViewById(R.id.loadErrorView) + if(viewStub!=null){ + loadErrorView=viewStub.inflate() + val loadErrorText=loadErrorView?.findViewById(R.id.loadErrorText) + loadErrorText?.text=tip + } + } + + //当因为网络原因无法显示的时候,显示该界面 + protected fun showBadNetworkView(listener:View.OnClickListener){ + if(badNetworkView!=null){ + badNetworkView?.visibility=View.VISIBLE + return + } + val stubView=findViewById(R.id.badNetworkView) + if(stubView!=null) { + badNetworkView=stubView.inflate() + val badNetworkRootView=badNetworkView?.findViewById(R.id.badNetworkRootView) + badNetworkRootView?.setOnClickListener(listener) + } + } + + //当获取到的数据为空时,显示该界面 + protected fun showNoContentView(tip:String){ + if(noContentView!=null){ + noContentView?.visibility=View.VISIBLE + return + } + val stubView=findViewById(R.id.noContentView) + if(stubView!=null) { + noContentView=stubView.inflate() + val noContentText=noContentView?.findViewById(R.id.noContentText) + noContentText?.text=tip + } + } + + //将noContentView隐藏 + protected fun hideNoContentView(){ + noContentView?.visibility=View.GONE + } + + //将loadErrorView隐藏 + protected fun hideLoadErrorView(){ + loadErrorView?.visibility=View.GONE + } + + //将badNetworkView隐藏 + protected fun hideBadNetworkView(){ + badNetworkView?.visibility=View.GONE + } + + fun showProgressDialog(title:String?,message:String){ + if(progressDialog==null){ + progressDialog=ProgressDialog(this).apply { + if(title!=null){ + setTitle(title) + } + setMessage(message) + setCancelable(false) + } + } + progressDialog?.show() + } + + fun closeProgressDialog(){ + progressDialog?.let { + if(it.isShowing){ + it.dismiss() + } + } + } + + companion object { + + private const val TAG = "BaseActivity" + } + } diff --git a/main/src/main/res/layout/bad_network_view.xml b/main/src/main/res/layout/bad_network_view.xml new file mode 100644 index 0000000..8a9eadb --- /dev/null +++ b/main/src/main/res/layout/bad_network_view.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/load_error_view.xml b/main/src/main/res/layout/load_error_view.xml new file mode 100644 index 0000000..01e65ed --- /dev/null +++ b/main/src/main/res/layout/load_error_view.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/no_content_view.xml b/main/src/main/res/layout/no_content_view.xml new file mode 100644 index 0000000..ff6af72 --- /dev/null +++ b/main/src/main/res/layout/no_content_view.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/view_stub_holder.xml b/main/src/main/res/layout/view_stub_holder.xml new file mode 100644 index 0000000..309c3e2 --- /dev/null +++ b/main/src/main/res/layout/view_stub_holder.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values-v21/transition_svg.xml b/main/src/main/res/values-v21/transition_svg.xml new file mode 100644 index 0000000..c2c9d5f --- /dev/null +++ b/main/src/main/res/values-v21/transition_svg.xml @@ -0,0 +1,5 @@ + + + + transition_logo_splash + \ No newline at end of file From cc7aa1e78725289514250cbe23fe000ad8e080d4 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Fri, 12 Apr 2019 18:48:22 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E7=BD=91=E7=BB=9C=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=96=B9=E9=9D=A2=E7=9A=84=E5=B0=81=E8=A3=85=EF=BC=8C=E8=BF=98?= =?UTF-8?q?=E6=B2=A1=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/vcs.xml | 1 + build.gradle | 1 + core/build.gradle | 5 +- .../java/com/example/core/util/GlobalUtil.kt | 14 +++ .../example/main/init/ui/SplashActivity.kt | 9 ++ network/build.gradle | 14 +-- .../com/quxianggif/network/model/Callback.kt | 15 +++ .../java/com/quxianggif/network/model/Init.kt | 44 +++++++++ .../com/quxianggif/network/model/Response.kt | 15 +++ .../quxianggif/network/request/InitRequest.kt | 14 +++ .../network/request/LoggingInterceptor.kt | 31 ++++++ .../com/quxianggif/network/request/Request.kt | 94 ++++++++++++++++++ .../java/com/quxianggif/network/util/MD5.java | 67 +++++++++++++ .../quxianggif/network/util/NetworkConst.kt | 99 +++++++++++++++++++ .../com/quxianggif/network/util/SignUtil.java | 44 +++++++++ .../com/quxianggif/network/util/Utility.kt | 94 ++++++++++++++++++ 16 files changed, 548 insertions(+), 13 deletions(-) create mode 100644 network/src/main/java/com/quxianggif/network/model/Callback.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/Init.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/Response.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/InitRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/LoggingInterceptor.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/Request.kt create mode 100644 network/src/main/java/com/quxianggif/network/util/MD5.java create mode 100644 network/src/main/java/com/quxianggif/network/util/NetworkConst.kt create mode 100644 network/src/main/java/com/quxianggif/network/util/SignUtil.java create mode 100644 network/src/main/java/com/quxianggif/network/util/Utility.kt diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..8306744 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5a6c9f9..c221c4f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.30' ext.kotlin_version = '1.3.21' repositories { google() diff --git a/core/build.gradle b/core/build.gradle index 822a6e8..8593b1c 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -31,12 +31,11 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api "com.android.support:appcompat-v7:${SUPPORT_LIB_VERSION}" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" api 'org.litepal.android:kotlin:2.1.0' api 'com.google.code.gson:gson:2.8.5' + api 'com.squareup.okhttp3:okhttp:3.10.0' + } repositories { mavenCentral() diff --git a/core/src/main/java/com/example/core/util/GlobalUtil.kt b/core/src/main/java/com/example/core/util/GlobalUtil.kt index db12a1a..0c9c5f0 100644 --- a/core/src/main/java/com/example/core/util/GlobalUtil.kt +++ b/core/src/main/java/com/example/core/util/GlobalUtil.kt @@ -1,7 +1,10 @@ package com.example.core.util +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.widget.Toast import com.example.core.GifFun +import com.example.core.extension.logWarn import java.text.SimpleDateFormat import java.util.* @@ -46,4 +49,15 @@ object GlobalUtil { e.printStackTrace() } } + + fun getApplicationMetaData(key:String):String?{ + var applicationInfo:ApplicationInfo?=null + try { + applicationInfo=GifFun.getContext().packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA) + }catch (e:PackageManager.NameNotFoundException){ + logWarn(TAG,e.message,e) + } + if(applicationInfo==null) return "" + return applicationInfo.metaData.getString(key) + } } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt index e67a2e6..0e8fe00 100644 --- a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt +++ b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt @@ -29,7 +29,11 @@ abstract class SplashActivity :BaseActivity(){ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enterTime=System.currentTimeMillis() + delayToForward() + } + override fun setupViews() { + startInitRequest() } //设置闪屏界面的最大延迟跳转,让用户不至于在闪屏界面等太久 @@ -65,6 +69,11 @@ abstract class SplashActivity :BaseActivity(){ } } + //开始向服务器发送初始化请求 + private fun startInitRequest(){ + + } + companion object { private const val TAG="SplashActivity" diff --git a/network/build.gradle b/network/build.gradle index edb9848..0948bc5 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -3,15 +3,13 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion COMPILE_SDK_VERSION as int defaultConfig { - minSdkVersion 15 - targetSdkVersion 28 - versionCode 1 - versionName "1.0" + minSdkVersion MIN_SDK_VERSION as int + targetSdkVersion TARGET_SDK_VERSION as int testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -28,11 +26,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'com.android.support:appcompat-v7:28.0.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation project(':core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } repositories { diff --git a/network/src/main/java/com/quxianggif/network/model/Callback.kt b/network/src/main/java/com/quxianggif/network/model/Callback.kt new file mode 100644 index 0000000..1efc359 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/Callback.kt @@ -0,0 +1,15 @@ +package com.quxianggif.network.model + +import java.lang.Exception + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午2:48 + * Describe:网络请求响应的回调接口 + */ +interface Callback { + + fun onResponse(response:Response) + + fun onFailure(e:Exception) +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/Init.kt b/network/src/main/java/com/quxianggif/network/model/Init.kt new file mode 100644 index 0000000..81e7f4c --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/Init.kt @@ -0,0 +1,44 @@ +package com.quxianggif.network.model + +import com.example.core.model.Version +import com.google.gson.annotations.SerializedName + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午2:26 + * Describe:初始化请求的实体类封装 + */ +class Init :Response(){ + + //基本的url地址头,应当根据返回的url地址头去组装所有后续的访问接口 + var base:String="" + + //新的token,重新延长有效期。只有在初始化时传入了老的token,才会有新的token返回 + var token:String="" + + //已登录用户的头像。只有在初始化时传入了正确的token,才会有返回此字段。 + var avatar:String="" + + /** + * 已登录用户的背景图。只有在初始化时传入了正确的token,才会有返回此字段。 + */ + @SerializedName("bg_image") + var bgImage: String = "" + + /** + * 是否存在版本更新。 + */ + @SerializedName("has_new_version") + var hasNewVersion = false + + /** + * 版本更新的具体信息。 + */ + var version: Version? = null + + companion object { + fun getResponse(callback: Callback){ + + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/Response.kt b/network/src/main/java/com/quxianggif/network/model/Response.kt new file mode 100644 index 0000000..464c4ae --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/Response.kt @@ -0,0 +1,15 @@ +package com.quxianggif.network.model + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午2:27 + * Describe:请求响应的基类,这里封装了所有请求都必须会响应的参数,status和msg + */ +open class Response { + + //状态码,这里可以查看所有状态码的含义:https://github.com/sharefunworks/giffun-server#2-状态码 + var status:Int=0 + + //请求结果的简单描述 + var msg:String="" +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/InitRequest.kt b/network/src/main/java/com/quxianggif/network/request/InitRequest.kt new file mode 100644 index 0000000..8710fa3 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/InitRequest.kt @@ -0,0 +1,14 @@ +package com.quxianggif.network.request + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午2:52 + * Describe:初始化请求。对应服务器接口:/init + */ +class InitRequest :Request(){ + + override fun method(): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/LoggingInterceptor.kt b/network/src/main/java/com/quxianggif/network/request/LoggingInterceptor.kt new file mode 100644 index 0000000..01d8636 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/LoggingInterceptor.kt @@ -0,0 +1,31 @@ +package com.quxianggif.network.request + +import com.example.core.extension.logVerbose +import okhttp3.Interceptor +import okhttp3.Response + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午3:17 + * Describe:Okttp网络请求日志拦截器,通过日志记录OkHttp所有请求以及响应的细节 + */ +internal class LoggingInterceptor :Interceptor{ + + companion object { + val TAG="LoggingInterceptor" + } + + override fun intercept(chain: Interceptor.Chain): Response { + val request=chain.request() + val t1=System.nanoTime() + logVerbose(TAG,"Sending request: "+request.url()+"\n"+request.headers()) + + val response=chain.proceed(request) + + val t2=System.nanoTime() + + logVerbose(TAG,"Received response for "+response.request().url()+" in "+(t2-t1)/1e6+"ms\n"+response.headers()) + + return response + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/Request.kt b/network/src/main/java/com/quxianggif/network/request/Request.kt new file mode 100644 index 0000000..96cf1c0 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/Request.kt @@ -0,0 +1,94 @@ +package com.quxianggif.network.request + +import com.quxianggif.network.model.Response +import com.quxianggif.network.util.Utility +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import java.util.concurrent.TimeUnit + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午2:56 + * Describe:网络请求模式基类,所有的请求封装都应该继承此类。这里会提供网络模块的配置,以及请求的具体逻辑处理等 + */ +abstract class Request { + + companion object { + const val GET = 0 + + const val POST = 1 + + const val PUT = 2 + + const val DELETE = 3 + } + + private lateinit var okHttpClient: OkHttpClient + + private val okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder().addNetworkInterceptor(LoggingInterceptor()) + + private var callback: Callback? = null + + private var params: Map? = null + + var getParamsAlready = false + + var deviceName: String + + var deviceSerial: String + + init { + connectTimeout(10) + writeTimeout(10) + readTimeout(10) + deviceName=Utility.deviceName + deviceSerial=Utility.getDeviceSerial() + } + + private fun build(){ + okHttpClient=okHttpBuilder.build() + } + + fun connectTimeout(seconds: Int) { + okHttpBuilder.connectTimeout(seconds.toLong(), TimeUnit.SECONDS) + } + + fun writeTimeout(seconds: Int) { + okHttpBuilder.writeTimeout(seconds.toLong(), TimeUnit.SECONDS) + } + + fun readTimeout(seconds: Int) { + okHttpBuilder.readTimeout(seconds.toLong(), TimeUnit.SECONDS) + } + + //设置响应回调接口 + fun setListener(callback: Callback?){ + this.callback=callback + } + + //组装网络请求后添加到HTTP发送队列,并监听响应回调 + fun inFlight(requestModel:Class){ + build() + val requestBuilder=Request.Builder() + if(method()==GET&&getParams()!=null){ + + } + } + + abstract fun method():Int + + //获取本次请求所携带的所有参数 + private fun getParams():Map?{ + if(!getParamsAlready){ + params=params() + getParamsAlready=true + } + return params + } + + open fun params():Map?{ + return null + } + +} diff --git a/network/src/main/java/com/quxianggif/network/util/MD5.java b/network/src/main/java/com/quxianggif/network/util/MD5.java new file mode 100644 index 0000000..e11072b --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/util/MD5.java @@ -0,0 +1,67 @@ +package com.quxianggif.network.util; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午4:08 + * Describe:MD5加密辅助工具类 + */ +public class MD5 { + private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * 对传入的字符串进行MD5加密 + * @param origin 原始字符串 + * @return 加密后的字符串 + */ + public static String encrypt(String origin){ + try { + MessageDigest digest=MessageDigest.getInstance("MD5"); + digest.update(origin.getBytes(Charset.defaultCharset())); + return new String(toHex(digest.digest())); + }catch (NoSuchAlgorithmException e){ + e.printStackTrace(); + } + return ""; + } + + /** + * 获取文件的MD5值 + * @param path 文件的路径 + * @return 文件的MD5值 + */ + public static String getFileMD5(String path){ + try { + FileInputStream fis=new FileInputStream(path); + MessageDigest md=MessageDigest.getInstance("MD5"); + byte[] buffer=new byte[1024]; + int length; + while ((length=fis.read(buffer,0,1024))!=-1){ + md.update(buffer,0,length); + } + BigInteger bigInteger=new BigInteger(1,md.digest()); + return bigInteger.toString(16).toUpperCase(); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + private static char[] toHex(byte[] data){ + char[] toDigits=DIGITS_UPPER; + int l=data.length; + char[] out=new char[l<<1]; + //two characters from the hex value + for(int i=0,j=0;i>>4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } +} diff --git a/network/src/main/java/com/quxianggif/network/util/NetworkConst.kt b/network/src/main/java/com/quxianggif/network/util/NetworkConst.kt new file mode 100644 index 0000000..3b11944 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/util/NetworkConst.kt @@ -0,0 +1,99 @@ +package com.quxianggif.network.util + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午6:00 + * Describe:网络通信模块的常量 + */ +interface NetworkConst { + + companion object { + const val OPEN_SPACE = 1 + + const val PRIVATE_SPACE = 2 + + const val UID = "u" + + const val DEVICE_NAME = "device" + + const val DEVICE_SERIAL = "d" + + const val CLIENT_VERSION = "cv" + + const val CLIENT_CHANNEL = "channel" + + const val TOKEN = "t" + + const val NUMBER = "number" + + const val VERIFY = "v" + + const val UUID = "ud" + + const val HEADER_USER_AGENT = "User-Agent" + + const val HEADER_USER_AGENT_VALUE = "GifFun Android" + + const val HEADER_APP_VERSION = "appv" + + const val HEADER_APP_SIGN = "apps" + + const val OPEN_ID = "openid" + + const val ACCESS_TOKEN = "access_token" + + const val NICKNAME = "nickname" + + const val CODE = "code" + + const val URI = "uri" + + const val SPACE = "space" + + const val COVER = "cover" + + const val GIF = "gif" + + const val GIF_MD5 = "gif_md5" + + const val CONTENT = "content" + + const val IMG_WIDTH = "img_width" + + const val IMG_HEIGHT = "img_height" + + const val LAST_FEED = "last_feed" + + const val URL = "url" + + const val FEED = "feed" + + const val COMMENT = "comment" + + const val REF_FEED = "ref_feed" + + const val LAST_COMMENT = "last_comment" + + const val USER_ID = "user_id" + + const val USER = "user" + + const val FOLLOWING_IDS = "following_ids" + + const val FOLLOWING_ID = "following_id" + + const val PAGE = "page" + + const val DESCRIPTION = "desp" + + const val AVATAR = "avatar" + + const val BG_IMAGE = "bg" + + const val LOADING_MORE = "loading_more" + + const val KEYWORD = "keyword" + + const val REASON = "reason" + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/util/SignUtil.java b/network/src/main/java/com/quxianggif/network/util/SignUtil.java new file mode 100644 index 0000000..9cd5fbe --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/util/SignUtil.java @@ -0,0 +1,44 @@ +package com.quxianggif.network.util; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.Signature; + +import com.example.core.GifFun; + +import java.security.MessageDigest; + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午4:38 + * Describe: + */ +public class SignUtil { + + private static final char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + //将字符串进行加密 + private static String toHexString(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte aB : b) { + sb.append(HEX_DIGITS[(aB & 0xf0) >>> 4]); + sb.append(HEX_DIGITS[aB & 0x0f]); + } + return sb.toString(); + } + + public static String getAppSignature() { + try { + PackageInfo info = GifFun.getContext().getPackageManager().getPackageInfo(GifFun.getContext().getPackageName(), PackageManager.GET_SIGNATURES); + MessageDigest digest = MessageDigest.getInstance("MD5"); + Signature[] signatures = info.signatures; + if (signatures != null) { + for (Signature s : signatures) + digest.update(s.toByteArray()); + } + return toHexString(digest.digest()); + } catch (Exception e) { + return ""; + } + } +} diff --git a/network/src/main/java/com/quxianggif/network/util/Utility.kt b/network/src/main/java/com/quxianggif/network/util/Utility.kt new file mode 100644 index 0000000..b2d98a2 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/util/Utility.kt @@ -0,0 +1,94 @@ +package com.quxianggif.network.util + +import android.annotation.SuppressLint +import android.os.Build +import android.provider.Settings +import android.text.TextUtils +import com.example.core.GifFun +import com.example.core.extension.logWarn +import com.example.core.util.GlobalUtil +import com.example.core.util.ShareUtil +import java.lang.Exception +import java.util.* + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/12 下午3:47 + * Describe:获取各项基础数据的工具类 + */ +object Utility { + + private val TAG = "Utility" + + private var deviceSerial: String? = null + + val deviceName: String + get() { + var deviceName = Build.BRAND + " " + Build.MODEL + if (TextUtils.isEmpty(deviceName)) { + deviceName = "unKnown" + } + return deviceName + } + + //获取当前app的版本号 + val appVersion: String + get() { + var version = "" + try { + val packageManager = GifFun.getContext().packageManager + val packInfo = packageManager.getPackageInfo(GifFun.getPackageName(), 0) + version = packInfo.versionName + } catch (e: Exception) { + logWarn("getAppVersion", e.message, e) + } + + if (TextUtils.isEmpty(version)) { + version = "unKnown" + } + return version + } + + //获取App网络请求验证参数,用于辨识是不是官方渠道的App。 + val appSign: String + get() { + return MD5.encrypt(SignUtil.getAppSignature() + appVersion) + } + + /** + * 获取设备的序列号。如果无法获取到设备的序列号则会生成一个随机的UUID来作为设备的序列号,UUID生成之后会存入缓存, + * 下次获取设备序列号的时候会优先从缓存中读取 + */ + + @SuppressLint("HardwareIds") + fun getDeviceSerial(): String { + if (deviceSerial == null) { + var deviceId: String? = null + val appChannel = GlobalUtil.getApplicationMetaData("APP_CHANNEL") + if ("google" != appChannel || "samsung" != appChannel) { + try { + deviceId = Settings.Secure.getString(GifFun.getContext().contentResolver, Settings.Secure.ANDROID_ID) + } catch (e: Exception) { + logWarn(TAG, "get android_id with error", e) + } + if (!TextUtils.isEmpty(deviceId) && deviceId!!.length < 255) { + deviceSerial = deviceId + return deviceSerial.toString() + } + } + var uuid = ShareUtil.read(NetworkConst.UUID, "") + if (!TextUtils.isEmpty(uuid)) { + deviceSerial = uuid + return deviceSerial.toString() + } + uuid = UUID.randomUUID().toString().replace("-", "").toUpperCase() + ShareUtil.save(NetworkConst.UUID, uuid) + deviceSerial = uuid + return deviceSerial.toString() + + } else { + return deviceSerial.toString() + } + } + +} \ No newline at end of file From 7171233f29073dab11b723fefe2d9b3c0bf79505 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Mon, 15 Apr 2019 20:02:54 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E9=97=AA=E5=B1=8F=E7=9A=84=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 27 +-- .../opensource/OpensourceSplashActivity.kt | 12 +- .../res/drawable-xxhdpi/follow_button.png | Bin 0 -> 2089 bytes .../drawable-xxhdpi/follow_button_pressed.png | Bin 0 -> 2168 bytes .../main/res/drawable-xxhdpi/logo_reverse.png | Bin 0 -> 6778 bytes .../drawable-xxhdpi/phone_login_button.png | Bin 0 -> 2744 bytes .../phone_login_button_pressed.png | Bin 0 -> 2779 bytes .../res/drawable-xxhdpi/user_home_page_bg.png | Bin 0 -> 8794 bytes app/src/main/res/layout/activity_splash.xml | 14 ++ app/src/main/res/values/colors.xml | 6 +- .../java/com/example/core/extension/Global.kt | 42 ++++ .../java/com/example/core/util/GlobalUtil.kt | 9 + .../example/main/event/ForceToLoginEvent.kt | 8 + .../com/example/main/event/MessageEvent.kt | 8 + .../example/main/init/ui/SplashActivity.kt | 49 +++++ .../com/example/main/util/ResponseHandler.kt | 65 ++++++ main/src/main/res/values/styles.xml | 9 + .../exception/ResponseCodeException.kt | 10 + .../java/com/quxianggif/network/model/Init.kt | 3 +- .../network/model/OriginThreadCallback.kt | 8 + .../quxianggif/network/request/InitRequest.kt | 46 ++++- .../com/quxianggif/network/request/Request.kt | 193 ++++++++++++++++-- .../com/quxianggif/network/util/AuthUtil.kt | 48 +++++ 23 files changed, 521 insertions(+), 36 deletions(-) create mode 100644 app/src/main/res/drawable-xxhdpi/follow_button.png create mode 100644 app/src/main/res/drawable-xxhdpi/follow_button_pressed.png create mode 100755 app/src/main/res/drawable-xxhdpi/logo_reverse.png create mode 100755 app/src/main/res/drawable-xxhdpi/phone_login_button.png create mode 100755 app/src/main/res/drawable-xxhdpi/phone_login_button_pressed.png create mode 100644 app/src/main/res/drawable-xxhdpi/user_home_page_bg.png create mode 100644 app/src/main/res/layout/activity_splash.xml create mode 100644 core/src/main/java/com/example/core/extension/Global.kt create mode 100644 main/src/main/java/com/example/main/event/ForceToLoginEvent.kt create mode 100644 main/src/main/java/com/example/main/event/MessageEvent.kt create mode 100644 main/src/main/java/com/example/main/util/ResponseHandler.kt create mode 100644 network/src/main/java/com/quxianggif/network/exception/ResponseCodeException.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/OriginThreadCallback.kt create mode 100644 network/src/main/java/com/quxianggif/network/util/AuthUtil.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bbc123..15ca85e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,30 +3,33 @@ package="com.example.quxiang"> - + - - - - + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt b/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt index e9723de..f840bfd 100644 --- a/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt +++ b/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt @@ -1,9 +1,19 @@ package com.example.quxiang.opensource +import android.os.Bundle +import com.example.main.init.ui.SplashActivity +import com.example.quxiang.R +import kotlinx.android.synthetic.main.activity_splash.* + /** * Anthor: Zhuangmingzhu * Date: 2019/4/2 下午7:05 * Describe:开源版闪屏Activity界面 */ -class OpensourceSplashActivity { +class OpensourceSplashActivity:SplashActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_splash) + logoView=logo + } } \ No newline at end of file diff --git a/app/src/main/res/drawable-xxhdpi/follow_button.png b/app/src/main/res/drawable-xxhdpi/follow_button.png new file mode 100644 index 0000000000000000000000000000000000000000..ce451a2be6c300de8023f24c9de3bc4f387f3e12 GIT binary patch literal 2089 zcmV+^2-f$BP) zK~#9!?43Pq6h{=t=PrCC{tyV;a%}l4DZrNE#+L>Jk}JE3B}Ykxk+?`95)CPm0t#H1 zBGf5@6e7h)gfu}$f+ntzVv#1Mh>@@n3jT=fME(#6`w|=B%lzj&&hC2mZr8ru+ui&B zq?5U0?{0SXKX2Z9^JXuR&1MBSGjT1Q5Q(9z$TkZ?B97GZoYeB*&Cc_~>N$iiMUE$D z@j`n=gJ^lPMpPsZvbRX&$ZW2Q*?*VCWM)CXbVG_gof!E2o3woLeX|tzxrS~fazdgG z#D0C@zL=ALC7&eWuHau3If)Z3TZVhfjTuV9C)e%@A#oEsv<;0X^$Uc=RqQ|soq;Eb zD_Yx0b_FR1D0D)h6GEXALZK5vp%X%(6GEXALZK5vp%V(75DJ};Sg33_KHSUV3POOLE{2%I$`|DCl^kF9RdP5D)D6sVErKsIs?fE^o z{{B_;*QP>cd?Mqzd^dkzb)cQd0rm)9ow+B*UOWl)K>gzBE!S{O}&M72DY8qk9CM?N%JPK$DsI(47O?uLr7I;f^f+CYE2Vs`mytfQ|h< zy7EX|e)f~AJ~di@?A5#@XI~24!&HdQ_ClCCSab5wQPFIe%CAeY=d+X$8bq$?Ir3g$ zZS6VIsa!z~+Tmul?5b`vwe!x>xVnC2DRcypS7+{;91s;szz8i*nZnzp4y8%;e z*fKE)LeDjJi?9D2RnOtRpq*9`V@A@}90TKY{fUya7r}k)fokPy!BRuGZf49nIZ!&} zIbp!$GgSC&{h7?V>>;}m2ETl1W9QTj@xr3xuM+#3dEjHp*K|}i+KQ#UvLO&F4=Qc2kMVIyK({g@4gE?{rFo~!K3v!bS5m;$-_rI zr`F&Sgkj+bLP1PY2xK(eh-jw)C@z>eAaZ?82A|zl0V#ZSv&-SGZ!1G`9U@`T(NVB2 zUIN$;>r$a;yVohd)B_aXjiA%$PQf2Xz@_c0^*q=+nmT?X;Ym;0bx)DAHPD zUl>BhEOeo%$JF05AxzcAQs|hbue|$_X%WK?SR)JyvNy2sgZ|bFT$ft=B5lnBab2w9 zMA~n5ec2L}9N00x@gumHymqZV90_TK+L22 zBy@L+DrT@6>g9bwEVy9cLF`6RLgZFsWyhQ<_qgZScDKf@Y@48>a7Vxr9{*BlfxdJJS^;CbqBZlMbn9_`mw zu~cF=W{u#2L_Nj0FH8d0TgQrt77ZR@*+31Ee>HR0Gy`G70%o3pw@+->o0LlIItbOI zY*LIA`YtZI;rAeYMQ@a^qTAOv^VJ6b4KfY{qB8^sTQJ=h*3 zdRG!2u(qkHwxU^iv>V@Lj2)1*GU8@`fzeegbi7@?d#r@+R4WlAUTWOESa96L#|{X` ztNW3=13<&p{c6nZg-erpy<55U6eQe-Sz70W$R3t;l0OO-Ry`gRpL z^#`$IRTkf2HzS76Dd$YI3Ae*z2ub0WKz Tz6Na_00000NkvXXu0mjf?-lZ+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/follow_button_pressed.png b/app/src/main/res/drawable-xxhdpi/follow_button_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea164c8cb45ac22a63a710a2c4bc70cdf2f07e3 GIT binary patch literal 2168 zcmV-;2#5EHP)F${xr=Rovs;1^a4|Ds@ zch5cN+;e*pg+f7q3sWPP6CyEO5QUT=B;rUdXQY;|-Z(gPO?wWZOOfA{kI&+Tp2iN5 z+SV!>liS!^By!}|--yhcCGj*juU@((#eOdF#pE}a<&&>>NpYX)=ohJmHue^YJek$3 zxII5E{*j`We3^v1f=3iNi4)K=G+lIKhLiBgHFt#~CovQ|v<;0XwHKt@LP#vdUevZG zbOxRzt|)CM*%hP~pwJ10P6&lg2!&1vg-!^CP6&lg2!&1vg-$4RLMU`XVnJOL8$JDc zM%zUK8k6;cWyT~{LLXatEbe9>iPiP|mcK#l$J=}5eP^i8A@^TiG`?3KiT=*6y%ueC zEw0})x-cb75Gff)xgz{d9= zXD@tzH7RuL&b~hTtKkL<;xrw)1#S#JCv03w9TN@7w=RHY*`?T>toi03+2d>M0l)lW z*D>j`^tyg8+?>{;WkZA=&}F|jFUSK1=3yw_igpfnyUp%t>TJOGiJLKI^qfpn6Vf3h6?}NuXrkw z>BX`J)OtP?et9;(yybO>7Zx4&@YSEL7zaM4e8oOF2&}}?)70UMm4^^h^{ob@p>N;2 z0QZPOj{y@epE=H65PPZk16CimE!I|emr6CK72~i*5qhEPBY9WWqN#%;t-@Idt_rN} z$sK)`BEQ&m)FB32+DI`X_(m$#)|E@t$4BT_p8QajKW#n^oe7I|_=CN!Q)>;{YH9Eb zM-U2Pl2Ra}QI3eJVE`Ow59vlhfI^$;|Q3~e>fPK6Ios?zRAlI2#u%aG}4Mbtwag(-?Tp z8GW5ERjFWuZ_5G6!zHda=ku*>c^w7p#8Rx>_2362LZ9&2gF zQUzT#@4D0?aILQF`>d>Et?KO68bZ)O+-j2x;vzMBAct>P@TIwJ#q&YB^oY%j1Gg*I zLbvLqQYWo^LRAxD_WRV2i{T#sKPtf!&g;?g?3UMW&y9YIj`hExJ=D8H1XvW>KEx=G6}_*UV$w^H>1KSUT%F<&X z>jTdt#f2_dc(h+x#h}El&laSz~aVz%<0pHV=O+Od+ zSc3n2*MM&eV$&-#hW3SZvw38M4nGi* zbu5g)$lrIwpme|Bf_WWt=(q8q41g=6?+B*o)qr*a!GDLlrKGrjnHv!nEsKTv)Ubm;ODub7-+EIU;Xv7<=-G2x9$f9B7*3**Q+tM7cLEoXsphgSaY0)r zkXA~cI;=$C=KqPh5CpQg(w9DPKeeN;YHi01gH>59SYZI6y<3l|Za&?w6;cY_17KlQ zDKZwVs^uP`k`Omls+?P2tNLZ?Aa<lsj^*pZj@p5BB==| zbW#%p=DIkQdJ+;Np|^ylgp<&9A%sFFghD5TLMMbmCxk*LghD5TLMIeDArv|x6gnXk zIw7GFdT#vV;&*u;fzY*YNy*0R{m0NvnP8k~1v;0000N0mvrCelBB|^lvSr^wmh5Xq z*6hnze$)5&$Md@9p4WZOdEfJ#=iK+bH`+i?gBrn#002;HX=054fDlCpfRhoMJDx>% zi4BE^rll7ElnnnK2uROlAu2C>t66v(yWjPGV&~}ulpWpeolsg2?3|s9oa`L^9(6m( z1HekIg;h53om|hN^ky_o?{tSQ&?Sbwl!t2hM(0LoU~HLg*(%enKn|MaV6<>EID zhq5BSy}HC(DYyE8My&2;>ngmcUWO8ZE>k3yC(X0^3Hd~{4tpY+`SdYQHl(E|sL zRmoUIsPaGNQ2)y?e1jslNCKHXKVWHCT$0PkQGzDpr4F#I?Ds5TyC92h*N zNV-G1j=)r(hk6HR?bIO*t#2<7{5^g^>^)vWt|Ow~!gEzio<)K;YYIx0XE#M3;B*7M zJ-jMAA9l0zZw{P^$Ehx=z2l^?Nawj#y{(eDO1W}OlEpPIej1x7BaR&}Vye0dC}A;y z>6DlU;U9D8=t<@?!prFaBbSGuJJ4m$a3Nh3$Q}$DHH3(n?I&SctNtK&oHfbllR@f= zB3n`_)5ES<9J1oOKP>1pph4XtodT_B!BcJeGCHDA(%vzFJ?W*?e{Tw_3PPeezI*3n zZH9q)fja~4A!EX~V*(=12luu_$4@K9jm~DSZ)Vpa%1b<7r`>p1vXVaeij3bUt&p%; z>=FT}&K`JZTr}O73D}Z3@snJ>QRFHwVzzFn;gFn01LL8Qw z?aF~=So5_l8srX^07pu-iu1Hm_mPfSpP~oyW9x+OvyP0-v+Z<3{FbvMf4O7HW^bw& z$fh}?v*z87FqmpOR9}2pG4?b=cYaiwy=I0WDtwzeT_l_Kcvo5IioiE&*v>~d2%e~f zDUs`O(qB6)p`7G(moxfZKOd;X73AJFJFNum>ScGTJES#LUKv==yzExjMzQ0aBO)P3 zirzbsQK0W?8>rJ#ApG9&;pzXE?qc~;Viuue8L16t=BT|E+ zP^r5#>Gtr+h1_q%>sU+jHWw}}UPS_4!B*4`eosQnje=8emDx%pb4WrIf@I1LmZJf%ci zIcjrG=Qg*jprxbFiXb8)(1vP#x^yCK&c)e9>A_p?a8yQIDKH(yuKFdI|&A zj8f)SUC#^E`ZLD_zlNW6i8FGcDQ+jnX0}9S%QialGDPrci^X%Sih`cVi?o*)6Om1H z%OWyOtpoiEk_N9!CCy)Hyu4Lx$ zU^2T%Fu>rlw@Sg2^Wnz{(Z2K>eLief-s&kG8QD*z4CGygS{En_VDElcU;-zE~%r%jxMTp0u0=ao+Md351< z^JeN@?7i>Y;JEn96o=HWk|Q}0>pHYvw!$TE5w7?<1r$&p)~`Me?6koT^b zl*0864L?rJak#brh(i2PlsrEVNSSP`J^Pe4a>T$BKsvA0C}So#X6A;xbmbuC`CKrs zNVLvr6~%TP5SrbNIQseifw<*}Dje3_&V4>x7xaB^Z)WO=-5M_=-o>&pAkO#gqUzj8 z`mK3VI~98BF{q}jjC$#X?{;Or;G_VO&4N~6(awKkdmH_ZM4+6X9)Z9AZHoYsliP5*xNPAh17PA6Oas%**GhHj zvoSGUoS=nJ^h;ghDwCo|V8oVv?beJ2%y$|-6F92XRUmLum|{i$_7r4m`;h;ln@I}| z$cmQD??GsN_FDo3!l3xF+Ec3nMJg&UriXsvACnD0EF2?cFj3p+$$MMC@HR+1e))Jk z=gpaGlH*)e>6U+n@N={Xun7|QxU1yr6;iuah${=Cg>%se@?$L_GBYFS0_C^XH@a(n z%>4{r3hE4-g6U*@WM|cmdP@q}Y+#aq}kK* zRr6eo`>;Wly2%v)gGb8i#J5-IJ6mFUPv&{ys^1Zl#dr4 zm76e%wudGrvAq~>qvSi@9ONhlWC_0{dIrOx59va-Cme+0&Yr44p4QXYGGfrc2a8JDhj;|b<>62X30Z8T z1m7jE&C3`@2B1pHH1i+2V%WbusPAI1-;4cGFo~QN_UUBD(zH?{Hm)l2DO9FeXUb;x zV|q-%QmbK>cOY)l?~u9CA17zq6cV1!LZANz98tDRKBLaBD!h|fKlrmtVR*_L>G$fj zYRQflLg>)Stor-GE~UF)Tg`eJ(h1hg#n>BId*NVs^7wPnJ$Zpf1)TKaY^Y2Q?L}fH z+vEd}zN~CRkO%ni&HYmO-9IG}C`RxVjn~wb(4D9Gw@eW}YleEN;|%~GS!N!O4r4rj7=PWk{_bnh2>Ux8#iDGp#cT5vWS$ z%^{XwEiq5NC_gm75?FsG0d%EK>WhTDFw7xC` z!)xxg@XHd0&OYzyvc7;~9P@(Sn`mr3SzW)kJ^JoGYbsHdYZ_w~KerqHQOCuCXfb4A z#@+igoBT4w07S8S>us<+Cy0!nO2p|vP!F9~@mk`ax2k!au8Y9%MTnlKvizDvpn<+x z<;O?odlPH_(_aN>s^SOo>LkV>D&x;%-NUpQ!l#NF7h42)B>BL2vSWRADq%A}L=d#~ zSLtiTO*AA?-Ld(cq+SS+HH#zX_&ndzinM<{(*{hV67CyT`u|h0pZrG0VIZjDP?(Ah`};TCXn-{(u35Qs{{(H zp4t+}NkRTDk2Tgr$*Kl7)5?s|vFKu^y9hXfX5W8j9(_JMH$8R|(WUPV&Z)5KAQs#i z$hx8D@ zMT0OIxaSyU%@_N%&%6ON+on8ZB+(dR)*-IYJ4kpQWxQS-Zk-ZVa-V6W<{5wUgGi?B zf9FoUsN!cP(dsMjVCZQ)eECGE#^h$KEY6WY{a=ln@bCCc-kYCvnJw=%$dICT@^&6s zX->ZCpME{Q{5iS3pEJO=%}x9RAx>LPiC8F1Y+hbiJCzIO;dcqn$KtaZ0g#mpsn>mN?_5?)5MNdap9InpImJ2^smXXID26 z(tNqQkrD5SuYIfG%BABGE9wQ?yF-WHk1s=fDNUJ6Ef!My4B2;y*?d2@Cg+8uDv?wd zilf1-qwC|#aY&w@O-hFkhort~6dD4!7Rt&?zPJQst zNHs4pl?`H&6lCWfcv!tvybgt7gar=SM3b@f)Oo~EQvcH^lsxG4G>@8Z{&qIbf(ENC zFCH=~Bk*5ym)FKhV!lhWWQk`|hd`UrnC^~@6ro`6RugF`0zYX8Qk#o4A0B3m12k}) zt^M3{i7fB1vydwcin;!83-Rt=3vrLsr~ov*J&X-#LxP|$Uwqs&Dwx+WxhKDhZ*?az?HV)EZvjaX6?7PG(9AP5j; z!E@^6_FHv#HSR=`DDWb}%2YQ)`%9yUgy&80g@dF!Q}^V!Qrb~}ZSb=+Sr-Hw1*|!v z;heZPEWNoR!QMepyK-6F#Nr)A#Z}Yv@Qa_C}#3Ra9USUDpFzCc}dnf)^i8qtUQQRM?JApf+)prvdW^rpt-=ln52%Xjx#wQ8R z$%CZA2ujLM*9+%i#!b*LzBA)0B&Vb@pTZ=l_%>qq25dr_U125b#w~^hxjn|=l6q4| z?-haKRa)X%LPy&nc2ddl@WNy2`J%-Vh2ejdZzl$ZZ$CjY5Mp1RDZiUH56 zt|b&|1`cq2+@I=@@{DhA-gBF;Os~^dw%EI!3XLohlHw$HSWNUU$psQO`}|U zK^*I8o(`RhjeowSZrHmepn)WGrM*>6E-9Y^E+R6H;KB}`B&>^L3w|!>^+Dgb z+GOTl>)>)+TXt1=^}>KHj^S0{#kNq-Q!f9l=&}LA#7aN`MNsRgD9SJj4WwJIzZ-64 zY`;ns)LCo~_rPzm*wAEuV7fTyrc7lMW5wA@P|`3^l*R_cp@9nA(2EsIPNbIcHlq8# z9AX`Tl|-Y%xyFrCvp}zds)FS_9+5KVP&`(fv!~k0q8`p&%k9g8Xco}(@|y`?J~SQI zf@mi0-sadPaXd|e&^pdE#?j@-Sx7*-w6~nDB0w318$*m{>yjac4u<-M?HP&mhBu5U zq~u~5@g<%V7;}c(L3y(+pgKAnZX(DBOgqx>ZDof_2KIh^Tn|6QaQ5^C*1FLFPxhn( z!miWKSw2@3ez-McGGfX4!Lx$8fg87}s-B6k-d^8ByNu^u9Y>*)5+C<^NakOv#j>`B zbawVXcB>p*_a690DkR>hNppaTrv4N+=AV+iQ6Jtv>nj$mw%0Ia^RE{eh%F;>*LvfoPSZt$b>7;~Uf2E&hD0Yr?`de}fSbMO2(+lhe<;zE z8{dDIrfz0;L5Ua|ROXGfM3=IK;-Ma0H%91{WIx!QqIR7S6#dEWw;QbyJ4mRIl=?vS z*a_=UwNplJ6Xg(vP~|}d;Db%jk~WEDY_hWTrCjhthd*+wRrejJZ;Fz6ZJW~H)91jJ zEzz*yx5o6bje|rWeU^$eC%im0&$V7pG4JnD@s z#X0AAP~RAdS338#YrQ2GZrsA8L&p_#+If*j3K%Ii>5$CawSoBqk8~6gE#EeO@hw|b zo@ma`xfvsgwvGzU95dSJp$!xpDa;M8GG7m=Y*il8S;AE=EQg$^7H)09R7{m4TAS{q zg&q7cfEv8HM)rvNQs?_yXW8bz%j5PhQtRaz!i^C7=W1r87*YYssDO^P7yC)(?g1Ms z>qPc~)vov?EcB7d+Al4!kzEfA_6y~2Op_$zSO^&t-3`W5@!4!5ezg*!sY3LSXr)T1 zZ7iGK_lV8r9@c#(Ev@W;3kE`bhgl1($})P8r-_$&Y8&j|kjk!v z@Zb$#@+~YwUQw(4g&6Gk-jBW^TML=8bdfX{NSSoDg5N>PvK!v-Y%4=(NbS49Hh1fN zJNxj`iAo5i3?u3xh=EI(< zZvwUQX$@ItKm^7&??`YgugMf576H}2|AWE16AN$tW#!pt))u$2u(2)rHQ*b9^p}>M zofp^p&Zo=>COALU#dADQ+>3Hl1X`oK%%VNRamm|*ozLivm8#1YY2a5nVtG3@Bq=>g zGQS!cr??Z|{`!0!4!W3L9$wOaJ+nl6f4cle@O%}@jNaY~qTgal!*>?(Obn)^M~NS_ zDU0GslquuFmk*&kTeanIjb(lv&ex$BIuzD^`H2cZL&G#zPxUB(8XifJHKzpu6N@{i zyw2qCXpXJ}eP(I;;BqFASP}zj7l;h5`d_&L*lD8JhhT0~6)F8LXE2H8#= zF4gvS7kv&$pD8y6Qx~9+6zsuo6$?c@Uk~>RfR+e^>i#vWz$ZV&pGWg$vjjLW{aHBu f{|lDWCO`rlLwn?s{Yr^fLI7x~>0wJ$Y(xJKaVLF? literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/phone_login_button.png b/app/src/main/res/drawable-xxhdpi/phone_login_button.png new file mode 100755 index 0000000000000000000000000000000000000000..795e9e5bb6e6266a0c692a0da2fdba3ee4dff737 GIT binary patch literal 2744 zcmZ8jeLRzU8^1+q%FFQ{7P02&^s>iM$#Is7va=iIB@voTMM*YN2_uD8I@jQQQ_kDeT*X#GX?(4pDclz$o*R#?? z5JaEk?dgvoIw-g=T)YVUw<)p1;DR~k9YRNtB_{Bvg%tBF0k|yDYfoZ8{DH*eNLn;! zbnxH(in4#z0ggm=t?ZNIvvSftZ8kITEN<82yg6`FPH(AN<(ex}_IG_+LvRW(%RfEc zs13Xfe$KBRrgn3%@5mH?Me(b-;f;{b=A`<+zod1muxs2}(qD;k(;Xu*0G@u4ts45w zR1JMC>FE*6?SrnlWf}wAveeDB=*EwXuCbAPUWmX?*fpO9V9NV<^iz&sOn8=FTvH z(>%+75J$@q%bjKRGGP?thS!%WaF)H@OO*dBLqcuVp2WhrDR*e8iWNr*Cv}@tHh7sP zej=G_CX1hCH0a>#!)$FJ<>hDW>$z{gC{j7{%fi2VQy?^&M(z;?C)fE=Mw(z<9{VPe zi0srtd1t=5syEpkgaeH&F)|F>IpjR_GX0k?nSNX!eV0E5*)31sAJcX3*1ExHQrBut zHEGXdV!Cdb;4S!W-Adu#F!7(0%BoGogUvIoagOK#O@`BZsn+#z5w{;@k&1Y4%z&0< zUa%k1_$W()+QO;VDr%m|lr3O8%XCvA!;5frc~jRdn#{SK(?(Fe{2AG=#guzlk_hLL zfUff`YBJ@bD%f7jnlZYcdvXDf8TRdsH9S6cM=m-{{5+5)3A_Vj#&=wzYM5?LEHUx@tiq0Lp!5!c)0D5v{j>g;)0(BQ81(T?}XEV&EoXw4>@17gPriH7@-ew)@0&I84Z9 zvru?>&($QhMN^L*0h%8&(pW84r(vbB)nB0YxF`cs;&JAG%#1Q58{?q6&oB4t;K`}# z+ejz1vlA052*Ms+05NQj`e+h8(5hzR<;;`&n z>0_H-&e=Y~uurts*#Llo+Aj$?O5ExG;i`}bjUAlhiPEtAfX0Wa2=&}$d?{64ouz!} zWWJsQI~=(n{>s-ayFw6ls4kU43++$j>fO)Xrv|b}P4Af+$oyIRivT~xaq`< zM`bA^*Tqzpa{DBZ8CvH%qdp9<@BI*8ItJ&4;8D*hJ-vnyf(w8R7S}Ynh8v$>L!?$- zQk!>1q}<(*A+ZYub=519@?hJbn9HQ}o4)!O(~GkrUcP^yOj>S2gJ$%`a?KQqv0 zFDp9*Gfdn=wP2#j%0!S%VeM=f-L``u{N$;Ra2h+4N|eo`OTp2Ru@u4YVGVI{HHT)G+_zv#<@al$qgzJH zwo)+-aYz4fy=xJ!SVf;7<3j4!z zbqwZHRt+vOrj;7t5>u$WHUyuarM(>0(6{99{@Sb8^qJdz=<|hPnU>(-oZ`Uz#&0f3 zgw+_YcI(^TJwhKHM}SMylXm)78mzf<@|R$a+}+X2&8Ki2;2vddkFT5@KjB@+kIx{z zwGX;Wy8OHP;8R;ryh5q|m|0JFmRL|dEKk1U1x#FpH~4xa!I!Tq&lz!_;`w@hSF_Y1 zN*9v^vYR%o7FPb0$EJ5lLczZIQx`byNRibVgkI5S@sHEPYYMw@p` z9XdyYqpHJ_9@3Jmxp%$npfn67;U#Xf+;yB$}C$IcEU)$k@M-~8JvULEa9S6V0pNg z3K_uc9+?`=%R1wxy&sjM5t&ndg07+gy%ckCndJJIf4HeoB&(A5^}Pz`ZtnmB{&pE< zbdF=SVW@SE#?c7AQl2d`g9awHnDkZ*Pg;t`x53b)h-3bfQL2Vcp|X4JyYcpF3Yx98wF{$|^4vtPEovg~PQ2@MMM)sG}viU$mGj2jy zPC2jG0D#g*-_7Pf`XOc8?2lvUtcuE#+iekOsH=*Q}@ezS~A)dQ@a7~E?tj$8wvFH2ySof5m z`QJQD`rCTmpYhbOhd2N@oo${@!OtDxY^h3@^IK$UK@GE(jDFg2Q-`rmIhaq5) zy3An1!lSog#rWpfzi2Sz`Kpp-@d`C#g|%Rr!{Q;m7HjZu?KVW^ zmcr^G`_E{|BcwaP+Z6FuNQ1-0ugx(sBOZqS*`Yh+roMOrJftt?|64Aja!a~^A7ow@ zszLde10;zTSF%LUP7}JKA#$CQPuveAvK_4JlNH<K6{()&zdU&g82X z_|T*D9>0v}&{4Yq(52*uJw_(PXZ{N}F0ZtQwO?}3l&O83m7vKcpmII5jODo~8+pH) zX_+6roFwldg5Rfdxv$CEzZz|yDdI8dO*X@odQkhgEe^F(!pZ=?r7H*3(D>r+RJkw&nLmz-E2=e1cIh z_Jq-59C{$rDs?Jv37&E50ybToI%oMQ;p@BUh(g`$e$FJy9k5yzl0YIDRyq?fj>Es=WyS`6}NITmMo# z3ugSVm7w{cQLzy~rJYeO%4d@qRrh;$m~BsZy#ZFu?aya5#?!N>G&A6I@z>7oFc&7B ze}3==oTV5AB^7e*$c>M_Ve%(I+}g&eVs-9Ve+>u`r5G_VMK39LtxA*;08pcuigp0z z9d;b%#$%HAq57Kghi6{H{@~}jU^OMywqlR$gkUQ{x6N?*iBTfVfx%LqAYPdkTV*+e zUYP7k8yCyY$EgEBNW|$ z6Hr;}3bT=z@E&LPdr}2)i79;@QSUnO`_J%@l_7eq0!*^{L-GT6mi1mdaSk3URjHSs&xvWvRysU{C>SuIB z=oLQ6qdUOQj9tK?Sbyln mv3E0tZNw=TyIRrm8eL@Y&j*`|lA=0+HA33$;acrNJ@X%n5G`W> literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/user_home_page_bg.png b/app/src/main/res/drawable-xxhdpi/user_home_page_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..71bf883efea8cf9609ebea7996b87fffbe53aaad GIT binary patch literal 8794 zcmW++cRbYpA3u9%-PayRRx(REoPAazBXZefClRupy>j->S)r`R3JIB6XYUzhWp9e# zyWbyopU-*lx%d0^dcK~|=j-(rrK7DzMb1JFfk3D<)RB7N`W(E}Nr}Nby)n%;xRAY6 zN4+dbwzuakFFuGzFGC~X0BG5!elETzzCZ4HQOki6u zQIB3<7g|BxmOv&zr_aCTLsx$pn;@!{UfRIaztHgYVo7hW-_0K#QS|#?|NC>)a&0^F zqMo6z-?Mz z=jR1D27JiI*~R4dB=S_jNT0K*In7QOmZlgO*yuv1U({C@IXq_~-fA4|a;<@pmI;tw zSBO!TS2$4wx1L-!o4eFLMbQv4s3Z_6T4*F1jY=3!)@Ec-wf|edPq!*KMmUL%a^k)zf-q{8XgTHj5S63(67S=@MUX<(>v3=XRFnZibpqA z8SR2*>yBAlb-c0W%)~2|0vd@^5?T|d&7H2#!KU*CKK1>yGMzlPV~7dM$N-U8fj`Br zT@6_>J)QLMl*=jK^}QYaE!!rO4{RH(ykbj8gRaR$B-JLvRD==iCK$GNEuVu+<%t3z zx3%qF8TK-yyo*^$V+&nNFWTt!@?SJgW*<$rm^X z^oj&{@Gv!osswyAFhHSFIW~lB7f(5yK?Z)nPkcMXT{$SHJhIIhi&vx~EGxw*g)!Ez z4HsU?IwRub+^^1+ja>-@669vG|8Nf6&A+e%A&}6jGZI3uh*O$* zH<*+;8v{)BemYYBur>2}ik$K*67@mh0(c zo~WPRJC<{&&0uFO%YOr00Cqt@K{?pkI?qZYQ*0cX<_AHfSY>l?*!X=IqEtu|0QU#H zu6=oF*Pur~Um45PJ$x9K0ZJ5X4dIfW;d=qcU}J&=1|A7c! z4YTx0u@(p52=Ld*b6|P>rlf>T^<3ty0_`T&^hbSteJNv8MYSapn`bbrveU{El{yW_ z{KMt0Z)r&{BV>x(MMZa2XY#b!*FYD#^Ihnnfx(rI?Jubq6`0kHh!9af?3!fO03t3u z!&#CQloVi#wcLmZlDGtKC z<-d4YUFYok(`dCgU*PSOqS=dWI(ikBZ(eLX`Od~IWSgJroc)Jm@+-l^=metZ1pDm- z@S^kg!a~HuEZ3(@Qw!xuvC>*eHmF}xanc)Z4O0Ovr(fcWK7dU9P9Wu@J3!Qb5}LpD zs5nXH0aG}(o$G<=q&NGxMC@c(8If4s?DlUzxmi#I!7B6WM*@C;a|{grG4dxN_Dt8W zzMf=q=f6j9Y3ZBqiBZbtt6%Uurdt6KJ@?z8pd`za<8C;xF`bm^@3+0{l=-WKp`8XP zgF!Po4g1RF_*X?SNm_bE z_5sc!?wP;KqaZ_v(HcBHq=C3Sr6H^{`^bK3Zf?%51f-%RLLIgXs7{6>P7+g4KI!yP zWu~qn@(2n>BM1ic6O!jh~kNjwi^fgQp6~?_xFd{ z8Qp?A2*q{7`|gf;Y{!4#rIu{4OJj2Bl(`jp%jCN#DD80r(e$G4G0(>Th!K0xedjz- z;K+kUu1rx8NhrAKJlN|I>ie^dHgOBm^L~(oz{%DQ@;6Kc`Gc zBvqJuvoZyC>nlCn-u}|xUE-ahfNBKtY^b0Bm;gSo~N3Gl~9F>R-xdH=y1m{ z6@cG5jPYU77v5cYk`vIVwfnRMY6XO7H;wInr zHY<#~!YtTG`B-33u??VAeRu3~5c1lswcf-jL-DP!oAsV#0hgRD^0%JHzl>s-VhR(K zQYrOMF?&p>=Ynqr_~578267oFTd<`%`Ev$5&-%q(QxSmW8^R`>)I|zm*&{?~?{6=^ z3)|ZE{T$)aT#l3P-Rn`VRK&6qEWuZOummK?HMZ_yjZvRw&qTEs&30)<%M>?rl@vT? z>uT55?97GSQcCE7)L)bl5_yvT(trEoZ^^8KQukIQeh-;6*S&t=U>YmR9awO$fK>U( zovO@idz#55+nfjb7hb=sEdj*t0|yob&I?6H^;M}PthUW-D;E2Zr3C3^wV4Sh|NYM zj#&$(G%A+5j?JPPw>`HgYMz$+(I)$(*=!7}n*9g}!ZudOzD|hn2uk>stnm%vFr+=Y z+K~PS!=*b*ZtU1t2?+?r&jL0JQ~mIXQdh}_5q7FRscuQM4n&EMVvlh`-EF;6&)@Z z3h^qoFxMI!?me@NRSGqg9va3Rz8QaUGS=-d^IMH3=<10RN23F1KEfOv&n~t)j`V z#@+|sw$6TzO=PigpNN(rB;1^@I3pW#$#4XhzZ(Y&()E@=Y-jjMvnsheG{~*RpdW*K zD-3Tlh5!|;ufHE5YmhDeo*`t8aG{f6xe(dy*>wDsj*O|UJ47+eRfv*o+jjV+#D5u< z>3-o`1O<(fB|iunX1pHD>a8iA1;(RUsMb~+Nf*p*(w1bdkCHT!5Y!)PDXG74>mvz7 zYjnL}HW@>gJQ^l6hW5|!^8UG+<(Dt3YDxrKh|7Te#c`5WxX0Yjiq%w9xYLlfCFnbT zLQYF1HN3tm^<9&aWZADIROd*qls2PP`c_EKNFLk4iSW7G!C|}k-=9cJ5=!7s4$tQd ztlyW^eQJBLNq9i==FvSdSO`*UwopiOlKFL0wwh=<)va66-7&T9^15C z`FlffI{VyePOZ_Y7xk|`kh{Vu+e`CUo+4Kj7edeqSBn1K=RljXwfPzAnC(Xp(TA;+ zyVJaU%=(1Yl{(8`cR}6y1_XT7yW4Zy26cfT+zX|JF)F-qi~w%>kTXULy1WnE1xVNi zzpWaYB{LKH?Lk5i^orFdHZSVS=*BbPePYcFQj#clwqL@%kfLdtzFuCH09Ey~(;ox> zI=y}i^l_-tA@{#2lX6221K@UhKe?~*ttmwU6fL}{#~ga`JSUB&$_m<6Kn%5|Uzcvp zL`7nnXb;wH{!H(lf+}LqkIt_=_Sxp3z6>t9eKhxrlp&3zKsYEO@u-#p;>#l#CBzV= zvgX_0UpHGGh-uVYqXB3wF6MVB*OAy#xUmvq!dMh3-A*_KOy}aCX=*-4+GKA8ai8N8 zX6tx`1gFQmu={h8gyIa;8XdGmV;VHgXFQ<9hf#6T!>`3$=Z(1J6gJNcB#jDha@0xyaZD zhBrRgXZ%jwtEHquYG8wm(RW_LOL%|Nk2u8P1Ehmym?;?nTv$?Jr`57p&HRgOk2b%1 z-EJ)pj`uxl!s2p>kYuDn_M5GvI>NQ+S1H$PZ>UE!UTQ1)BHrVb>@n*>8PGmG`jcP}2)aJpYa%5VBF$;XrZQQEMp&KP~pZZNcA)f-ah?A%~7V^#HX zoPbaBGzdo@rmjsH==d5kU+rxCF-MHhgC$cSAATnpQ=mMXcMJ=~D6T(WeKO6fnly@} z4pR8Z*FZP;`20PISn10Cy1ufGn*f}e!U^B3hv-eNtgNheO?(Y7anzi0UVp4e@M2Jj(dI_m{SjG^Yg5zD+SH z=Mp`tqHJE=_WWv0c=JJTLoRU11GXG^{cy4P;7Po+F+0$LJJvP^ z(~%oF7a#e%WDRs_^l!KyU|A@X6-(}7yyS?3^7T{qWGiLL3qc>K%uX13Suixt@%BdX zCk^BwBH^P@;_l<`^Bvb`-FU_48O96@+qPB-T?`)WeeXp;Y+Y|1{ErpwuZ2wm$vylvy$W|dP zij)*UY(Uca@?i?l&6+^l>ghBn%`YC*-5TRux@V2Gns1EA+wn!x39Ty7R#0n&s=TKD z@K?&o_|CIFkB4FnhzR5UUQD*^NHDJCqu2KYIB*`(`7eKReCFcR3$K(}_54G1Gf96~ zNt|PzaI<&7c7?brAxJG?%56KSgtxGIQp zzCYw`f4k|S`z+19H!rx9*4l3;+vF)Ihuk9eXr)_!C$VJa<@NM-E16LI#d?hB*Uxm; zQ4hH_Bh0(ic>yE`zyA1 zl326YRxZC%ijW`rHByz79ZAb90f%%Py_9#0}2dg420{?*?zBd$5j*rq?iWp{@u#?e5UzXxjXMb+Fn^(c~Nt8Y+~zp z{9AkP`&R;~)BGw*YlrRu9(}_1axgYqFw6u(x=S@#V7>^ee@8|9&8KhNTTar~czD)S zJ>CNFWCjJPgc6y19`AzLqG)NU(dycaXNjf(Tbx%G`Gt5X2#z?pB($2nF@)4Y(c@#~ zjo8H2Vuyn}L76dITim(mN*W|{sM71KrFn%a&jjY7HnMfddq-`nh~vC-kQ>w5MEDCl z(ow@0jwr<);!JUlmnF0$c={h6sEssq$u@O_N;%%Smy(E&2oAFm_ozoK^s?P_RMv%_ z?Cn{^bpCMcjof`EJ~gC;RnBa4@LB?m^1vhZzM|qzD>^Qu#ISGHqE0u8CqF5G9T|Pf z&8-~pnL|~XomO_`T{&9IjMZ`EQ1&s=CEGL-C|O@ng>BTFpJyz|-s~ADb$pvye)Z;@bDxYeJJ} zDBFnkWyfksn$RbKf6j*K?+Cli&npceV~F5j##vJGsng1Jyt4?T8DG-!>HGiY;yCIO zZKlUVKfRF1%~oEHCB@t*v7yk-RnF5$ggf!2=p-rHiqU<^t#Eo0SW52>6(nb<36fF#5I=>Y%u!PQRS;f#D()HXHLmtFAX80(OUocZ*cq&*|f;1#$mUQAt z-EQy@hpe;-!E}SGjynjjDN(n5^i7_+QcE(U7f}c zxi=2$+?Rode{cQ<-vYM~`?H*Zl9s|2T_VQk@;5x-+wV6=kla5yQUAdJ0n zUqOuuD(UW}`X8Yw60;KGkr%L~?N9D+1j`q>;B}1ER$bhA{mVx}d3v(kl$z<+)>bLS z?9nb-Bb%t9R`a>-$)7*J8Ic;0g>)-}k3k0eOH2`-5ezs>Ch2Y41fV~%jtBwa;;tp8 z=X)Gg;CI}wM^`M}&D z<-P>UI5#!Q33~}zl*V=h46eyx5tumGx3yaSB7Y*7c75cPBpLsr&6_3Kv60jI~Wbbwjl&idZuq`pS(g#aNN&SArP>$jwiKg4i zD)a&IJYA_{ziqXv7uTtHKLMHL^IcC29e>a{U2_@HZygWyLE_=^3Tdj&fcuzUNaqm^ z6FWC3>2^Rs^dAA!>V5L7S(=j2k(&%3QTxWvgo!dIMa?U+*MJg>a3B7aInkD@>mqNeS)^D}5^SFS|8|MyLEWtgJI(MLA&6*wbi|JSuV+|GrsoYNX4{lug9XJ( zaDm^$Vh&&KX!N&pTLOkYES;!QE3xz;l(40&$4p~LApjaA`)n#pwmMp zC4OJjZ&jV-xbtYG{00LBJTm3>o|(kq>})?#smIhfByUhN-91tQIG0qIf0vF26EA7U zRx-&`{XCXi;sprqz^xw1ca&D1PH9U?IJxXSL)nY}+7ekU#lm$asw9@knaJ9Q>Ra*I zpObNmZZepJ+t&;he413|=eoy~@!_T;%-IA#X^!bKebXo2>O_FouB)t$Ee~%cChg;RmF;0EXHEA zsX#yPO$J+`TF+)h1_;;x?c$J`?8}&l@Ei1rOK`5f*v&EeepSYt4?NN4X-`+ZEk?SQ2i!`1~Vxj%}N&sa1Z7hi0R`Hy?Pc5Y3Ze+>}x*VFr^4sj;3lQQUe2 z_+fNR^OBdmO_l08nilBb_y(mn?+B!4zKr5M47YKoW}xfe-cipMDH01?e=i+m*<!DO+6H|dTww7PoXyXZ zFhs8ucq1!zB8ybxQ*rvt%$~doFfxtCF6Ldf9rY`G$zdiMyyDG|)ccaD6(%9rP(SW? zT&N5~mBBF|p_s|ZC$I11qUyW9t$scG5mQP(4&*d5QjW#WpsFoioY+nJoFcz~KoypR zx!c-$%L9G!@ssA>w)r2g5o;|=pL%1fJr-GfQa#02T5(gue=Q$a*4h=uDyFB_{qbh{ z&nL*qjdjh(e(?dL!lQ-o=}q6BC@@vP=H#$v{%gf1%$T@n?o)=;8@k`ca=2EkIQYD4 zNM()}8zBje<0>)+RCj*5n3X&8#&)YnZYcqw$kmIgXN$&13`zZj*}C6dWqpBPrB8jM zjo|dMId06AO_;j4{q73fEH-9ipZo!x7!463{!MYUy|hD_G*>zA0&Xp2mF-6`jp!hMqOJ?=4HZt2U+MQSz2vUC3=X9%oM=}( zQm~0uVJ}t|{PFkgTdfTU+b!!af5~XKwQlusKS^NpxX$d=^HIWA4{}Y0ZHO8UwdnnL zw}!u^I69WjMmkWtVKNjpv8@Gky}=gVp< z;6P)GjtWld!=V#))y4OUDg8!s<%S=iHRrvM*v$(1>}w0x57+2dvg<@rJD%Z5o@9wE zro__JK4|;dI*vkx6>zfnZwj|)+|)6~ZLt&3@>+@zjJasZ^_P@i_a#02uJ#^QDfwSg zn}5J?kOseOnToHrz^S{dWuB0nse^NwQhCtr`HPahFoGlh-8ET3=!R4Cq*E0fp>>*P>62d-P{)g*IN1`#+X%2>546@SNhNxJ zbdmjb06FO6?Y+P7Uw3z^&BlOw0AWx*i`SU7qc~*(@-SX@G$-(?0URiSc{5h@_>J=T zHPe>zvz|}=|By+$*?23|M>ED=c8OWpMUvHS1@v6Z7`!cm+9A$o);;>K@F>cgx#m@S zz3v#l`Yh*vTlP3s<((OK%er{km^zZA0gqqj<_I!W*zpp-=z|`#kd!WqB@*%P%(B76 zXV7{sb}jctr~0btJWJ{2_aXSPy5~-;*-E)-u*>z2Zh9WPQo+B(H^(?=v8n!me zS{QYYB=fA};EwK-9?oDmpJ1 zDpJ93!ny3|15B&OaP@w`n-t$-wp=5->>le?>xoM+49L00Eo#reN4?3evKm7XT|e|g zzm?P!=1M69syvE&T8QRrD2QB|9`jyB<>QB!tPO1C#V@sFoGXed?1J0wnVtUUhIL>ByC2t-3g8(E=b5&S=e Cr`s3+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..e880d66 --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 69b2233..47d6960 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - #008577 - #00574B - #D81B60 + #58ae6b + #488252 + #5bd296 diff --git a/core/src/main/java/com/example/core/extension/Global.kt b/core/src/main/java/com/example/core/extension/Global.kt new file mode 100644 index 0000000..12f4191 --- /dev/null +++ b/core/src/main/java/com/example/core/extension/Global.kt @@ -0,0 +1,42 @@ +package com.example.core.extension + +import android.annotation.SuppressLint +import android.os.Looper +import android.widget.Toast +import com.example.core.GifFun + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午7:24 + * Describe:弹出Toast扩展工具 + */ + +private var toast:Toast?=null + +//弹出Toast信息,如果不是在主线程中就调用此方法,Toast信息将不会显示 +@SuppressLint("ShowToast") +@JvmOverloads +fun showToast(content:String,duration:Int= Toast.LENGTH_SHORT){ + if(Looper.myLooper()== Looper.getMainLooper()){ + if(toast==null){ + toast= Toast.makeText(GifFun.getContext(),content,duration) + }else{ + toast?.setText(content) + } + toast?.show() + } +} + +//切换到主线程后弹出Toast信息,此方法不管是主线程还是子线程,都可以成功弹出Toast信息 +@SuppressLint("ShowToast") +@JvmOverloads +fun showToastOnUiThread(content:String,duration:Int= Toast.LENGTH_SHORT){ + GifFun.getHandler().post { + if(toast==null){ + toast= Toast.makeText(GifFun.getContext(),content,duration) + }else{ + toast?.setText(content) + } + toast?.show() + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/util/GlobalUtil.kt b/core/src/main/java/com/example/core/util/GlobalUtil.kt index 0c9c5f0..0b5476a 100644 --- a/core/src/main/java/com/example/core/util/GlobalUtil.kt +++ b/core/src/main/java/com/example/core/util/GlobalUtil.kt @@ -50,6 +50,11 @@ object GlobalUtil { } } + //获取资源文件中定义的字符串。 + fun getString(resId:Int):String{ + return GifFun.getContext().resources.getString(resId) + } + fun getApplicationMetaData(key:String):String?{ var applicationInfo:ApplicationInfo?=null try { @@ -60,4 +65,8 @@ object GlobalUtil { if(applicationInfo==null) return "" return applicationInfo.metaData.getString(key) } + + fun getResponseClue(status:Int,msg:String):String{ + return "code: $status , msg: $msg" + } } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/event/ForceToLoginEvent.kt b/main/src/main/java/com/example/main/event/ForceToLoginEvent.kt new file mode 100644 index 0000000..ff0a420 --- /dev/null +++ b/main/src/main/java/com/example/main/event/ForceToLoginEvent.kt @@ -0,0 +1,8 @@ +package com.example.main.event + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午7:36 + * Describe:强制用户返回登录界面的消息类。 + */ +class ForceToLoginEvent:MessageEvent() \ No newline at end of file diff --git a/main/src/main/java/com/example/main/event/MessageEvent.kt b/main/src/main/java/com/example/main/event/MessageEvent.kt new file mode 100644 index 0000000..089970d --- /dev/null +++ b/main/src/main/java/com/example/main/event/MessageEvent.kt @@ -0,0 +1,8 @@ +package com.example.main.event + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午7:37 + * Describe:EventBus基类 + */ +open class MessageEvent \ No newline at end of file diff --git a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt index 0e8fe00..3258e81 100644 --- a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt +++ b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt @@ -1,13 +1,22 @@ package com.example.main.init.ui import android.os.Bundle +import android.text.TextUtils import android.view.View +import com.example.core.Const import com.example.core.GifFun +import com.example.core.extension.logWarn import com.example.core.model.Version import com.example.core.util.GlobalUtil +import com.example.core.util.ShareUtil import com.example.main.feeds.ui.MainActivity import com.example.main.login.ui.LoginActivity import com.example.main.ui.BaseActivity +import com.example.main.util.ResponseHandler +import com.quxianggif.network.model.Init +import com.quxianggif.network.model.OriginThreadCallback +import com.quxianggif.network.model.Response +import java.lang.Exception /** * Anthor: Zhuangmingzhu @@ -71,7 +80,47 @@ abstract class SplashActivity :BaseActivity(){ //开始向服务器发送初始化请求 private fun startInitRequest(){ + Init.getResponse(object :OriginThreadCallback{ + override fun onResponse(response: Response) { + if(activity==null){ + return + } + var version:Version?=null + val init=response as Init + GifFun.BASE_URL=init.base + if(!ResponseHandler.handleResponse(init)){ + val status=init.status + if(status==0){ + val token=init.token + val avatar=init.avatar + val bgImage=init.bgImage + hasNewVersion=init.hasNewVersion + if(hasNewVersion){ + version=init.version + } + if(!TextUtils.isEmpty(token)){ + ShareUtil.save(Const.Auth.TOKEN, token) + if (!TextUtils.isEmpty(avatar)) { + ShareUtil.save(Const.User.AVATAR, avatar) + } + if (!TextUtils.isEmpty(bgImage)) { + ShareUtil.save(Const.User.BG_IMAGE, bgImage) + } + GifFun.refreshLoginState() + } + }else{ + logWarn(TAG, GlobalUtil.getResponseClue(status, init.msg)) + } + } + forwardToNextActivity(hasNewVersion,version) + } + + override fun onFailure(e: Exception) { + logWarn(TAG,e.message,e) + forwardToNextActivity(false,null) + } + }) } companion object { diff --git a/main/src/main/java/com/example/main/util/ResponseHandler.kt b/main/src/main/java/com/example/main/util/ResponseHandler.kt new file mode 100644 index 0000000..1ad6d5a --- /dev/null +++ b/main/src/main/java/com/example/main/util/ResponseHandler.kt @@ -0,0 +1,65 @@ +package com.example.main.util + +import com.example.core.GifFun +import com.example.core.extension.logWarn +import com.example.core.extension.showToastOnUiThread +import com.example.core.util.GlobalUtil +import com.example.main.R +import com.example.main.event.ForceToLoginEvent +import com.quxianggif.network.exception.ResponseCodeException +import com.quxianggif.network.model.Response +import org.greenrobot.eventbus.EventBus +import java.lang.Exception +import java.net.ConnectException +import java.net.NoRouteToHostException +import java.net.SocketTimeoutException + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午7:20 + * Describe:对服务器的返回进行相应的逻辑处理。注意此类只处理公众的返回逻辑,射击具体的业务逻辑,仍然交由接口调用处自行处理 + */ +object ResponseHandler { + + private val TAG = "ResponseHandler" + + //当网络请求正常相应的时候,根据状态码处理通用部分的逻辑 + fun handleResponse(response: Response?):Boolean{ + if(response==null){ + logWarn(TAG,"handleResponse: response is null") + showToastOnUiThread(GlobalUtil.getString(R.string.unknown_error)) + return true + } + val status=response.status + when(status){ + 10001, 10002, 10003->{ + logWarn(TAG, "handleResponse: status code is $status") + GifFun.logout() + showToastOnUiThread(GlobalUtil.getString(R.string.login_status_expired)) + val event=ForceToLoginEvent() + EventBus.getDefault().post(event) + return true + } + 19000->{ + logWarn(TAG, "handleResponse: status code is 19000") + showToastOnUiThread(GlobalUtil.getString(R.string.unknown_error)) + return true + } + else->return false + } + } + + //当网络请求没有正常响应的时候,根据异常类型进行相应的处理 + fun handleFailure(e:Exception){ + when(e){ + is ConnectException -> showToastOnUiThread(GlobalUtil.getString(R.string.network_connect_error)) + is SocketTimeoutException -> showToastOnUiThread(GlobalUtil.getString(R.string.network_connect_timeout)) + is ResponseCodeException -> showToastOnUiThread(GlobalUtil.getString(R.string.network_response_code_error) + e.responseCode) + is NoRouteToHostException -> showToastOnUiThread(GlobalUtil.getString(R.string.no_route_to_host)) + else -> { + logWarn(TAG, "handleFailure exception is $e") + showToastOnUiThread(GlobalUtil.getString(R.string.unknown_error)) + } + } + } +} \ No newline at end of file diff --git a/main/src/main/res/values/styles.xml b/main/src/main/res/values/styles.xml index a79d0bb..abba0ef 100644 --- a/main/src/main/res/values/styles.xml +++ b/main/src/main/res/values/styles.xml @@ -1,7 +1,16 @@ + + + + + \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/exception/ResponseCodeException.kt b/network/src/main/java/com/quxianggif/network/exception/ResponseCodeException.kt new file mode 100644 index 0000000..abaea8e --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/exception/ResponseCodeException.kt @@ -0,0 +1,10 @@ +package com.quxianggif.network.exception + +import java.lang.RuntimeException + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午2:23 + * Describe:当服务器响应的头不在200与300之间时,说明请求出现了异常,此时应该将此异常主动抛出。 + */ +class ResponseCodeException(val responseCode:Int):RuntimeException("Http request failed with response code $responseCode") \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/Init.kt b/network/src/main/java/com/quxianggif/network/model/Init.kt index 81e7f4c..71be6da 100644 --- a/network/src/main/java/com/quxianggif/network/model/Init.kt +++ b/network/src/main/java/com/quxianggif/network/model/Init.kt @@ -2,6 +2,7 @@ package com.quxianggif.network.model import com.example.core.model.Version import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.InitRequest /** * Anthor: Zhuangmingzhu @@ -38,7 +39,7 @@ class Init :Response(){ companion object { fun getResponse(callback: Callback){ - + InitRequest().listen(callback) } } } \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/OriginThreadCallback.kt b/network/src/main/java/com/quxianggif/network/model/OriginThreadCallback.kt new file mode 100644 index 0000000..abf7b1f --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/OriginThreadCallback.kt @@ -0,0 +1,8 @@ +package com.quxianggif.network.model + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午2:03 + * Describe:网络请求响应的回调接口,回调时保留原来线程进行回调,不切换到主线程 + */ +interface OriginThreadCallback :Callback \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/InitRequest.kt b/network/src/main/java/com/quxianggif/network/request/InitRequest.kt index 8710fa3..a866806 100644 --- a/network/src/main/java/com/quxianggif/network/request/InitRequest.kt +++ b/network/src/main/java/com/quxianggif/network/request/InitRequest.kt @@ -1,5 +1,12 @@ package com.quxianggif.network.request +import com.example.core.GifFun +import com.example.core.util.GlobalUtil +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.Init +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + /** * Anthor: Zhuangmingzhu * Date: 2019/4/12 下午2:52 @@ -7,8 +14,45 @@ package com.quxianggif.network.request */ class InitRequest :Request(){ + companion object { + private val URL=GifFun.BASE_URL+"/init" + } + + init { + connectTimeout(5) + readTimeout(5) + writeTimeout(5) + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(Init::class.java) + } + override fun method(): Int { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return Request.GET + } + + override fun params(): Map? { + val params=HashMap() + params[NetworkConst.CLIENT_VERSION]=GlobalUtil.appVersionCode.toString() + val appChannel=GlobalUtil.getApplicationMetaData("APP_CHANNEL") + if(appChannel!=null){ + params[NetworkConst.CLIENT_VERSION]=appChannel + } + if(buildAuthParams(params)){ + params[NetworkConst.DEVICE_NAME] = deviceName + } + return params + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.UID,NetworkConst.TOKEN) + return super.headers(builder) } } \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/Request.kt b/network/src/main/java/com/quxianggif/network/request/Request.kt index 96cf1c0..c51d839 100644 --- a/network/src/main/java/com/quxianggif/network/request/Request.kt +++ b/network/src/main/java/com/quxianggif/network/request/Request.kt @@ -1,10 +1,20 @@ package com.quxianggif.network.request +import com.example.core.GifFun +import com.example.core.extension.logVerbose +import com.google.gson.GsonBuilder +import com.quxianggif.network.exception.ResponseCodeException +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.OriginThreadCallback import com.quxianggif.network.model.Response +import com.quxianggif.network.util.AuthUtil +import com.quxianggif.network.util.NetworkConst import com.quxianggif.network.util.Utility -import okhttp3.Callback -import okhttp3.OkHttpClient +import okhttp3.* import okhttp3.Request +import java.io.IOException +import java.lang.Exception +import java.lang.StringBuilder import java.util.concurrent.TimeUnit /** @@ -28,7 +38,7 @@ abstract class Request { private val okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder().addNetworkInterceptor(LoggingInterceptor()) - private var callback: Callback? = null + private var callback:Callback? = null private var params: Map? = null @@ -42,12 +52,12 @@ abstract class Request { connectTimeout(10) writeTimeout(10) readTimeout(10) - deviceName=Utility.deviceName - deviceSerial=Utility.getDeviceSerial() + deviceName = Utility.deviceName + deviceSerial = Utility.getDeviceSerial() } - private fun build(){ - okHttpClient=okHttpBuilder.build() + private fun build() { + okHttpClient = okHttpBuilder.build() } fun connectTimeout(seconds: Int) { @@ -63,32 +73,179 @@ abstract class Request { } //设置响应回调接口 - fun setListener(callback: Callback?){ - this.callback=callback + fun setListener(callback: Callback?) { + this.callback = callback } //组装网络请求后添加到HTTP发送队列,并监听响应回调 - fun inFlight(requestModel:Class){ + fun inFlight(requestModel: Class) { build() - val requestBuilder=Request.Builder() - if(method()==GET&&getParams()!=null){ + val requestBuilder = Request.Builder() + if (method() == GET && getParams() != null) { + requestBuilder.url(urlWithParam()) + }else{ + requestBuilder.url(url()) + } + requestBuilder.headers(headers(Headers.Builder()).build()) + when{ + method()== POST->requestBuilder.post(formBody()) + method()== PUT->requestBuilder.put(formBody()) + method()== DELETE->requestBuilder.delete(formBody()) + } + okHttpClient.newCall(requestBuilder.build()).enqueue(object:okhttp3.Callback{ + override fun onFailure(call: Call, e: IOException) { + notifyFailure(e) + } + + override fun onResponse(call: Call, response: okhttp3.Response) { + try { + if(response.isSuccessful){ + val body=response.body() + val result=if(body!=null){ + body.string() + }else{ + "" + } + logVerbose(LoggingInterceptor.TAG,result) + val gson=GsonBuilder().disableHtmlEscaping().create() + val responseModel=gson.fromJson(result,requestModel) + response.close() + notifyResponse(responseModel) + }else{ + notifyFailure(ResponseCodeException(response.code())) + } + }catch (e:Exception){ + notifyFailure(e) + } + } + + }) + } + + //当get请求携带参数的时候,将参数以key=value的形式拼装到get请求url的后面,并且中间以?符号隔开。 + private fun urlWithParam(): String { + val params = getParams() + if (params != null) { + val keys=params.keys + if(!keys.isEmpty()){ + val paramsBuilder=StringBuilder() + var needAnd=false + for(key in keys){ + if(needAnd){ + paramsBuilder.append("&") + } + paramsBuilder.append(key).append("=").append(params[key]) + needAnd=true + } + return url()+"?"+paramsBuilder.toString() + } + } + return url() + } + + //Android客户端的所有请求都需要添加User_Agent:GifFun Android这样一个请求头,每个接口的封装子类可以添加自己的请求头 + open fun headers(builder:Headers.Builder):Headers.Builder{ + builder.add(NetworkConst.HEADER_USER_AGENT,NetworkConst.HEADER_USER_AGENT_VALUE) + builder.add(NetworkConst.HEADER_APP_VERSION,Utility.appVersion) + builder.add(NetworkConst.HEADER_APP_SIGN,Utility.appSign) + return builder + } + + //构建post,put,delete请求的参数体 + private fun formBody():FormBody{ + val builder=FormBody.Builder() + val params=getParams() + if(params!=null){ + val keys=params.keys + if(!keys.isEmpty()){ + for(key in keys){ + val value=params[key] + if(value!=null){ + builder.add(key,value) + } + } + } + } + return builder.build() + } + //当请求响应成功,将服务器响应转换后的实体类进行回调 + private fun notifyResponse(response: Response){ + callback?.let { + if(it is OriginThreadCallback){ + it.onResponse(response) + callback=null + }else{ + GifFun.getHandler().post { + it.onResponse(response) + callback=null + } + } } } - abstract fun method():Int + //当请求响应失败的时候,将具体的异常进行回调 + private fun notifyFailure(e:Exception){ + callback?.let { + if (it is OriginThreadCallback) { + it.onFailure(e) + callback = null + } else { + GifFun.getHandler().post { + it.onFailure(e) + callback = null + } + } + } + } + + abstract fun method(): Int + + abstract fun url():String + + abstract fun listen(callback: Callback?) //获取本次请求所携带的所有参数 - private fun getParams():Map?{ - if(!getParamsAlready){ - params=params() - getParamsAlready=true + private fun getParams(): Map? { + if (!getParamsAlready) { + params = params() + getParamsAlready = true } return params } - open fun params():Map?{ + open fun params(): Map? { return null } + //构建和服务器身份证相关的请求参数 + fun buildAuthParams(params:MutableMap?):Boolean{ + if(params!=null&&AuthUtil.isLogin){ + val userId=AuthUtil.userId.toString() + val token=AuthUtil.token + params[NetworkConst.UID]=userId + params[NetworkConst.DEVICE_SERIAL]=deviceSerial + params[NetworkConst.TOKEN]=token + return true + } + return false + } + + //根据传入的keys构建用于进行服务器验证的参数,并添加到请求头中 + fun buildAuthHeaders(builder: Headers.Builder?,vararg keys:String){ + if(builder!=null&&keys.isNotEmpty()){ + val params= mutableListOf() + for(i in keys.indices){ + val key=keys[i] + getParams()?.let { + val p=it[key] + if(p!=null){ + params.add(p) + } + } + } + builder.add(NetworkConst.VERIFY,AuthUtil.getServerVerifyCode(*params.toTypedArray())) + } + } + } diff --git a/network/src/main/java/com/quxianggif/network/util/AuthUtil.kt b/network/src/main/java/com/quxianggif/network/util/AuthUtil.kt new file mode 100644 index 0000000..e409bdf --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/util/AuthUtil.kt @@ -0,0 +1,48 @@ +package com.quxianggif.network.util + +import android.text.TextUtils +import com.example.core.Const +import com.example.core.util.ShareUtil +import java.lang.StringBuilder + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/15 下午3:24 + * Describe:服务器身份验证相关的工具类 + */ +object AuthUtil { + + //判断用户是否已登录 + val isLogin:Boolean + get() { + val u=ShareUtil.read(Const.Auth.USER_ID,0L) + val t=ShareUtil.read(Const.Auth.TOKEN,"") + val lt=ShareUtil.read(Const.Auth.LOGIN_TYPE,-1) + return u>0&&!TextUtils.isEmpty(t)&<>=0 + } + + //获取当前登录用户id + val userId:Long + get() = ShareUtil.read(Const.Auth.USER_ID,0L) + + //获取当前登录用户的token。 + val token:String + get() = ShareUtil.read(Const.Auth.TOKEN,"") + + //获取服务器校验码。使用和服务器端相同的算法生成服务器校验码,对接口的安全性进行保护,防止对服务器进行恶意攻击 + fun getServerVerifyCode(vararg params:String):String{ + if(params.isNotEmpty()){ + val builder=StringBuilder() + var needSeparator=false + for(param in params){ + if(needSeparator){ + builder.append(",") + } + builder.append(param) + needSeparator=true + } + return MD5.encrypt(builder.toString()) + } + return "" + } +} \ No newline at end of file From fb9237ec31a6787fd7121bef8e0928bb254c3d89 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Mon, 22 Apr 2019 19:59:54 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E9=97=AA=E5=B1=8F=E7=9A=84=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E4=BB=A5=E5=8F=8A=E5=BC=80=E5=A7=8B=E7=99=BB=E9=99=86?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 22 ++- .../quxiang/opensource/MainActivity.java | 15 -- .../opensource/OpenSourceLoginActivity.kt | 22 +++ ...ctivity.kt => OpenSourceSplashActivity.kt} | 2 +- app/src/main/res/layout/activity_login.xml | 5 + main/src/main/AndroidManifest.xml | 9 +- .../example/main/event/FinishActivityEvent.kt | 11 ++ .../example/main/init/ui/SplashActivity.kt | 154 +++++++++++------- .../example/main/login/ui/LoginActivity.kt | 24 ++- .../java/com/example/main/ui/BaseActivity.kt | 15 ++ main/src/main/res/values-v21/styles.xml | 16 ++ main/src/main/res/values/styles.xml | 16 +- .../main/res/xml/network_security_config.xml | 10 ++ 14 files changed, 235 insertions(+), 96 deletions(-) delete mode 100644 app/src/main/java/com/example/quxiang/opensource/MainActivity.java create mode 100644 app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt rename app/src/main/java/com/example/quxiang/opensource/{OpensourceSplashActivity.kt => OpenSourceSplashActivity.kt} (90%) create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 main/src/main/java/com/example/main/event/FinishActivityEvent.kt create mode 100644 main/src/main/res/values-v21/styles.xml create mode 100644 main/src/main/res/xml/network_security_config.xml diff --git a/app/build.gradle b/app/build.gradle index 30baaa6..f224979 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,13 +31,5 @@ androidExtensions { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':main') - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} -repositories { - mavenCentral() -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15ca85e..197434c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,14 +4,19 @@ + + + @@ -21,6 +26,21 @@ + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/quxiang/opensource/MainActivity.java b/app/src/main/java/com/example/quxiang/opensource/MainActivity.java deleted file mode 100644 index 07eb8d7..0000000 --- a/app/src/main/java/com/example/quxiang/opensource/MainActivity.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.quxiang.opensource; - -import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; - -import com.example.quxiang.R; - -public class MainActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - } -} diff --git a/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt b/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt new file mode 100644 index 0000000..67394b3 --- /dev/null +++ b/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt @@ -0,0 +1,22 @@ +package com.example.quxiang.opensource + +import android.os.Bundle +import com.example.main.login.ui.LoginActivity +import com.example.quxiang.R + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/22 下午5:55 + * Describe:开源版界面登录,支持手机号登录,如果登陆的账号没有注册就会跳转到注册界面如果已经注册过了就直接会跳转到主界面 + */ +class OpenSourceLoginActivity :LoginActivity(){ + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + } + override fun forwardToMainActivity() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt b/app/src/main/java/com/example/quxiang/opensource/OpenSourceSplashActivity.kt similarity index 90% rename from app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt rename to app/src/main/java/com/example/quxiang/opensource/OpenSourceSplashActivity.kt index f840bfd..439d75b 100644 --- a/app/src/main/java/com/example/quxiang/opensource/OpensourceSplashActivity.kt +++ b/app/src/main/java/com/example/quxiang/opensource/OpenSourceSplashActivity.kt @@ -10,7 +10,7 @@ import kotlinx.android.synthetic.main.activity_splash.* * Date: 2019/4/2 下午7:05 * Describe:开源版闪屏Activity界面 */ -class OpensourceSplashActivity:SplashActivity() { +class OpenSourceSplashActivity:SplashActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_splash) diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..8469a44 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index b1af6fc..c60ef4b 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -1,2 +1,9 @@ + package="com.example.main" > + + + + + + + diff --git a/main/src/main/java/com/example/main/event/FinishActivityEvent.kt b/main/src/main/java/com/example/main/event/FinishActivityEvent.kt new file mode 100644 index 0000000..41d6e27 --- /dev/null +++ b/main/src/main/java/com/example/main/event/FinishActivityEvent.kt @@ -0,0 +1,11 @@ +package com.example.main.event + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/22 上午10:41 + * Describe:销毁Activity事件 + */ +class FinishActivityEvent :MessageEvent() { + + var activityClass:Class<*>?=null +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt index 3258e81..89b1727 100644 --- a/main/src/main/java/com/example/main/init/ui/SplashActivity.kt +++ b/main/src/main/java/com/example/main/init/ui/SplashActivity.kt @@ -3,12 +3,17 @@ package com.example.main.init.ui import android.os.Bundle import android.text.TextUtils import android.view.View +import android.widget.Toast import com.example.core.Const import com.example.core.GifFun +import com.example.core.extension.logError +import com.example.core.extension.logVerbose import com.example.core.extension.logWarn import com.example.core.model.Version import com.example.core.util.GlobalUtil import com.example.core.util.ShareUtil +import com.example.main.event.FinishActivityEvent +import com.example.main.event.MessageEvent import com.example.main.feeds.ui.MainActivity import com.example.main.login.ui.LoginActivity import com.example.main.ui.BaseActivity @@ -16,6 +21,8 @@ import com.example.main.util.ResponseHandler import com.quxianggif.network.model.Init import com.quxianggif.network.model.OriginThreadCallback import com.quxianggif.network.model.Response +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.lang.Exception /** @@ -24,20 +31,23 @@ import java.lang.Exception * Describe:闪屏Activity */ abstract class SplashActivity :BaseActivity(){ + /** + * 记录进入SplashActivity的时间。 + */ + var enterTime: Long = 0 - //记录进入SplashActivity的时间 - var enterTime:Long=0 + /** + * 判断是否正在跳转或已经跳转到下一个界面。 + */ + var isForwarding = false - //判断是否正在跳转或者已经跳转到下一个界面 - var isForwarding=false - - var hasNewVersion=false + var hasNewVersion = false lateinit var logoView: View override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enterTime=System.currentTimeMillis() + enterTime = System.currentTimeMillis() delayToForward() } @@ -45,60 +55,44 @@ abstract class SplashActivity :BaseActivity(){ startInitRequest() } - //设置闪屏界面的最大延迟跳转,让用户不至于在闪屏界面等太久 - private fun delayToForward(){ - Thread(Runnable { - GlobalUtil.sleep(MAX_WAIT_TIME.toLong()) - forwardToNextActivity(false,null) - }).start() + override fun onBackPressed() { + // 屏蔽手机的返回键 } - //跳转到下一个Activity.如果闪屏界面停留的时间还不足最短时间,要等待一会,保证闪屏不会一闪而过 - open fun forwardToNextActivity(hasNewVersion:Boolean,version: Version?){ - if(!isForwarding){ - isForwarding=true - val currentTime=System.currentTimeMillis() - val timeSpent=currentTime-enterTime - if(timeSpent< MIN_WAIT_TIME){ - GlobalUtil.sleep(MIN_WAIT_TIME-timeSpent) - } - runOnUiThread{ - if(GifFun.isLogin()){ - MainActivity.actionStart(this) + @Subscribe(threadMode = ThreadMode.MAIN) + override fun onMessageEvent(messageEvent: MessageEvent) { + if (messageEvent is FinishActivityEvent) { + if (javaClass == messageEvent.activityClass) { + if (!isFinishing) { finish() - }else{ - if(isActive){ - LoginActivity.actionStartWithTransition(this,logoView,hasNewVersion,version) - }else{ - LoginActivity.actionStart(this,hasNewVersion,version) - finish() - } } } } } - //开始向服务器发送初始化请求 - private fun startInitRequest(){ - Init.getResponse(object :OriginThreadCallback{ + /** + * 开始向服务器发送初始化请求。 + */ + private fun startInitRequest() { + Init.getResponse(object : OriginThreadCallback { override fun onResponse(response: Response) { - if(activity==null){ + if (activity == null) { return } - var version:Version?=null - val init=response as Init - GifFun.BASE_URL=init.base - if(!ResponseHandler.handleResponse(init)){ - val status=init.status - if(status==0){ - val token=init.token - val avatar=init.avatar - val bgImage=init.bgImage - hasNewVersion=init.hasNewVersion - if(hasNewVersion){ - version=init.version + var version: Version? = null + val init = response as Init + GifFun.BASE_URL = init.base + if (!ResponseHandler.handleResponse(init)) { + val status = init.status + if (status == 0) { + val token = init.token + val avatar = init.avatar + val bgImage = init.bgImage + hasNewVersion = init.hasNewVersion + if (hasNewVersion) { + version = init.version } - if(!TextUtils.isEmpty(token)){ + if (!TextUtils.isEmpty(token)) { ShareUtil.save(Const.Auth.TOKEN, token) if (!TextUtils.isEmpty(avatar)) { ShareUtil.save(Const.User.AVATAR, avatar) @@ -108,28 +102,70 @@ abstract class SplashActivity :BaseActivity(){ } GifFun.refreshLoginState() } - }else{ + } else { logWarn(TAG, GlobalUtil.getResponseClue(status, init.msg)) } } - forwardToNextActivity(hasNewVersion,version) + forwardToNextActivity(hasNewVersion, version) } override fun onFailure(e: Exception) { - logWarn(TAG,e.message,e) - forwardToNextActivity(false,null) + logWarn(TAG, e.message, e) + forwardToNextActivity(false, null) } - }) } + /** + * 设置闪屏界面的最大延迟跳转,让用户不至于在闪屏界面等待太久。 + */ + private fun delayToForward() { + Thread(Runnable { + GlobalUtil.sleep(MAX_WAIT_TIME.toLong()) + forwardToNextActivity(false, null) + }).start() + } + + /** + * 跳转到下一个Activity。如果在闪屏界面停留的时间还不足规定最短停留时间,则会在这里等待一会,保证闪屏界面不至于一闪而过。 + */ + @Synchronized + open fun forwardToNextActivity(hasNewVersion: Boolean, version: Version?) { + if (!isForwarding) { // 如果正在跳转或已经跳转到下一个界面,则不再重复执行跳转 + isForwarding = true + val currentTime = System.currentTimeMillis() + val timeSpent = currentTime - enterTime + if (timeSpent < MIN_WAIT_TIME) { + GlobalUtil.sleep(MIN_WAIT_TIME - timeSpent) + } + runOnUiThread { + if (GifFun.isLogin()) { + MainActivity.actionStart(this) + finish() + } else { + if (isActive) { + LoginActivity.actionStartWithTransition(this, logoView, hasNewVersion, version) + } else { + LoginActivity.actionStart(this, hasNewVersion, version) + finish() + } + } + } + } + } + companion object { - private const val TAG="SplashActivity" - //闪屏的最短时间 - const val MIN_WAIT_TIME=2000 + private const val TAG = "SplashActivity" + + /** + * 应用程序在闪屏界面最短的停留时间。 + */ + const val MIN_WAIT_TIME = 2000 - //闪屏的最长时间 - const val MAX_WAIT_TIME=5000 + /** + * 应用程序在闪屏界面最长的停留时间。 + */ + const val MAX_WAIT_TIME = 5000 } } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/login/ui/LoginActivity.kt b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt index 5426040..e28fc14 100644 --- a/main/src/main/java/com/example/main/login/ui/LoginActivity.kt +++ b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt @@ -3,8 +3,13 @@ package com.example.main.login.ui import android.app.Activity import android.app.ActivityOptions import android.content.Intent +import android.os.Build +import android.support.annotation.RequiresApi +import android.support.v4.content.ContextCompat.startActivity import android.view.View +import android.widget.Toast import com.example.core.GifFun +import com.example.core.extension.logError import com.example.core.model.Version import com.example.core.util.AndroidVersion import com.example.main.R @@ -39,16 +44,17 @@ abstract class LoginActivity :AuthActivity() { } //启动LoginActivity并附带Transition动画 - fun actionStartWithTransition(activity: Activity,logo: View,hasNewVersion:Boolean,version:Version?){ - val intent=Intent(ACTION_LOGIN_WITH_TRANSITION).apply { - putExtra(INTENT_HAS_NEW_VERSION,hasNewVersion) - putExtra(INTENT_VERSION,version) + fun actionStartWithTransition(activity: Activity, logo: View, hasNewVersion:Boolean, version:Version?){ + val intent = Intent(ACTION_LOGIN_WITH_TRANSITION).apply { + putExtra(INTENT_HAS_NEW_VERSION, hasNewVersion) + putExtra(INTENT_VERSION, version) } - if(AndroidVersion.hasLollipop()){ - intent.putExtra(START_WITH_TRANSITION,true) - val options=ActivityOptions.makeSceneTransitionAnimation(activity,logo,activity.getString(R.string.transition_logo_splash)) - activity.startActivity(intent,options.toBundle()) - }else{ + if (AndroidVersion.hasLollipop()) { + intent.putExtra(START_WITH_TRANSITION, true) + val options = ActivityOptions.makeSceneTransitionAnimation(activity, logo, + activity.getString(R.string.transition_logo_splash)) + activity.startActivity(intent, options.toBundle()) + } else { activity.startActivity(intent) activity.finish() } diff --git a/main/src/main/java/com/example/main/ui/BaseActivity.kt b/main/src/main/java/com/example/main/ui/BaseActivity.kt index 2f5eb87..6a362eb 100644 --- a/main/src/main/java/com/example/main/ui/BaseActivity.kt +++ b/main/src/main/java/com/example/main/ui/BaseActivity.kt @@ -21,10 +21,15 @@ import android.widget.TextView import com.example.core.util.AndroidVersion import com.example.main.R import com.example.main.common.callback.PermissionListener +import com.example.main.event.ForceToLoginEvent +import com.example.main.event.MessageEvent +import com.example.main.login.ui.LoginActivity import com.example.main.util.ActivityCollector import com.umeng.analytics.MobclickAgent import kotlinx.android.synthetic.main.activity_main.view.* import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.lang.ref.WeakReference /** @@ -228,6 +233,16 @@ open class BaseActivity :AppCompatActivity() { } } + @Subscribe(threadMode = ThreadMode.MAIN) + open fun onMessageEvent(messageEvent:MessageEvent){ + if(messageEvent is ForceToLoginEvent){ + if(isActive){//判断Activity是否在前台,防止非前台的Activity也处理这个事件,造成打开多个LoginActivity的问题。 + ActivityCollector.finishAll() + LoginActivity.actionStart(this,false,null) + } + } + } + companion object { private const val TAG = "BaseActivity" diff --git a/main/src/main/res/values-v21/styles.xml b/main/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000..01cb596 --- /dev/null +++ b/main/src/main/res/values-v21/styles.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values/styles.xml b/main/src/main/res/values/styles.xml index abba0ef..032ab22 100644 --- a/main/src/main/res/values/styles.xml +++ b/main/src/main/res/values/styles.xml @@ -2,7 +2,11 @@ + + + \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FetchVCode.kt b/network/src/main/java/com/quxianggif/network/model/FetchVCode.kt new file mode 100644 index 0000000..d82e781 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FetchVCode.kt @@ -0,0 +1,17 @@ +package com.quxianggif.network.model + +import com.quxianggif.network.request.FetchVCodeRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 上午10:13 + * Describe:获取短信验证码请求的实体类封装 + */ +class FetchVCode:Response() { + + companion object { + fun getResponse(number:String,callback:Callback){ + FetchVCodeRequest().number(number).listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/GetBaseInfo.kt b/network/src/main/java/com/quxianggif/network/model/GetBaseInfo.kt new file mode 100644 index 0000000..e17338f --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/GetBaseInfo.kt @@ -0,0 +1,33 @@ +package com.quxianggif.network.model + +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.GetBaseInfoRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 下午3:21 + * Describe:获取当前登录用户基本信息请求的实体类封装 + */ +class GetBaseInfo :Response(){ + + //当前登录用户的昵称 + var nickname:String="" + + //当前登录用户的头像 + var avatar:String="" + + /** + * 当前登录用户的个人简介。 + */ + var description: String = "" + + //当前登录用户个人主页的背景图。 + @SerializedName("bg_image") + var bgImage:String="" + + companion object { + fun getResponse(callback: Callback){ + GetBaseInfoRequest().listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/PhoneLogin.kt b/network/src/main/java/com/quxianggif/network/model/PhoneLogin.kt new file mode 100644 index 0000000..f9b45fa --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/PhoneLogin.kt @@ -0,0 +1,29 @@ +package com.quxianggif.network.model + +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.model.Response +import com.quxianggif.network.request.PhoneLoginRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 下午2:19 + * Describe:手机号登录的实体类封装 + */ +class PhoneLogin :Response(){ + + @SerializedName("user_id") + var userId:Long=0 + + + //记录用户的登录身份,token有效期30天 + var token="" + + companion object { + fun getResponse(number:String,code:String,callback:Callback){ + PhoneLoginRequest() + .number(number) + .code(code) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FetchVCodeRequest.kt b/network/src/main/java/com/quxianggif/network/request/FetchVCodeRequest.kt new file mode 100644 index 0000000..1e85c33 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FetchVCodeRequest.kt @@ -0,0 +1,54 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FetchVCode +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 上午10:20 + * Describe:获取短信验证码请求。对应服务器接口:/login/fetch_verify_code + */ +class FetchVCodeRequest :Request(){ + + companion object { + private val URL=GifFun.BASE_URL+"/login/fetch_verify_code" + } + + private var number="" + + fun number(number:String):FetchVCodeRequest{ + this.number=number + return this + } + + override fun method(): Int { + return Request.POST + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FetchVCode::class.java) + } + + override fun params(): Map? { + val params=HashMap() + params[NetworkConst.NUMBER]=number + params[NetworkConst.DEVICE_NAME] = deviceName + params[NetworkConst.DEVICE_SERIAL] = deviceSerial + return params + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.DEVICE_NAME,NetworkConst.NUMBER, NetworkConst.DEVICE_SERIAL) + return super.headers(builder) + } + + +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/GetBaseInfoRequest.kt b/network/src/main/java/com/quxianggif/network/request/GetBaseInfoRequest.kt new file mode 100644 index 0000000..a984973 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/GetBaseInfoRequest.kt @@ -0,0 +1,48 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.GetBaseInfo +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers +import java.util.HashMap + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 下午3:28 + * Describe:获取当前用户的基本信息请求。对应服务器的接口:/user/baseinfo + */ +class GetBaseInfoRequest :Request(){ + + companion object { + + private val URL = GifFun.BASE_URL + "/user/baseinfo" + } + + override fun method(): Int { + return Request.GET + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(GetBaseInfo::class.java) + } + + override fun params(): Map? { + val params = HashMap() + return if (buildAuthParams(params)) { + params + } else super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder, NetworkConst.DEVICE_SERIAL, NetworkConst.TOKEN) + return super.headers(builder) + } + + +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/PhoneLoginRequest.kt b/network/src/main/java/com/quxianggif/network/request/PhoneLoginRequest.kt new file mode 100644 index 0000000..83ad54c --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/PhoneLoginRequest.kt @@ -0,0 +1,54 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.PhoneLogin +import com.quxianggif.network.util.NetworkConst + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/4/24 下午2:26 + * Describe:使用手机号登录请求。对应服务器接口:/login/phone + */ +class PhoneLoginRequest :Request(){ + + companion object { + private val URL = GifFun.BASE_URL + "/login/phone" + } + + private var number:String="" + + private var code:String="" + + fun number(number:String):PhoneLoginRequest{ + this.number=number + return this + } + + fun code(code:String):PhoneLoginRequest{ + this.code=code + return this + } + + override fun method(): Int { + return Request.POST + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(PhoneLogin::class.java) + } + + override fun params(): Map? { + val params=HashMap() + params[NetworkConst.NUMBER] = number + params[NetworkConst.CODE] = code + params[NetworkConst.DEVICE_NAME] = deviceName + params[NetworkConst.DEVICE_SERIAL] = deviceSerial + return params + } +} \ No newline at end of file From 0d891d209ecd7f690d83fd10222e72e13105d969 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Thu, 25 Apr 2019 15:58:06 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E7=99=BB=E5=BD=95=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=83=A8=E5=88=86=E5=B7=B2=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- .../opensource/OpenSourceLoginActivity.kt | 18 ++- .../opensource/OpenSourceRegisterActivity.kt | 112 +++++++++++++++++- main/src/main/AndroidManifest.xml | 8 ++ .../com/example/main/feeds/ui/MainActivity.kt | 3 +- .../example/main/login/ui/LoginActivity.kt | 22 ++++ .../example/main/login/ui/RegisterActivity.kt | 99 ++++++++++++++++ main/src/main/res/drawable/move_on_bg.xml | 8 ++ .../src/main/res/layout/activity_register.xml | 81 +++++++++++++ main/src/main/res/values-v21/styles.xml | 4 + main/src/main/res/values/styles.xml | 4 + .../quxianggif/network/model/BaseRegister.kt | 21 ++++ .../quxianggif/network/model/PhoneRegister.kt | 22 ++++ .../network/request/PhoneRegisterRequest.kt | 52 ++++++++ 14 files changed, 444 insertions(+), 12 deletions(-) create mode 100644 main/src/main/res/drawable/move_on_bg.xml create mode 100644 main/src/main/res/layout/activity_register.xml create mode 100644 network/src/main/java/com/quxianggif/network/model/BaseRegister.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/PhoneRegister.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/PhoneRegisterRequest.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..37a7509 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt b/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt index 2a3312b..59f64c9 100644 --- a/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt +++ b/app/src/main/java/com/example/quxiang/opensource/OpenSourceLoginActivity.kt @@ -172,14 +172,24 @@ class OpenSourceLoginActivity :LoginActivity(){ } 10101->{ hideSoftKeyboard() - + OpenSourceRegisterActivity.registerByPhone(this@OpenSourceLoginActivity,number,code) + loginInProgress(false) + } + else->{ + logWarn(TAG, "Login failed. " + GlobalUtil.getResponseClue(status, msg)) + showToast(response.msg) + loginInProgress(false) } } + }else{ + loginInProgress(false) } } override fun onFailure(e: Exception) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + logWarn(TAG, e.message, e) + ResponseHandler.handleFailure(e) + loginInProgress(false) } }) @@ -200,10 +210,6 @@ class OpenSourceLoginActivity :LoginActivity(){ } } - override fun forwardToMainActivity() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - inner class SMSTimer(millisInFuture:Long,countDownInterval:Long):CountDownTimer(millisInFuture,countDownInterval){ override fun onFinish() { diff --git a/app/src/main/java/com/example/quxiang/opensource/OpenSourceRegisterActivity.kt b/app/src/main/java/com/example/quxiang/opensource/OpenSourceRegisterActivity.kt index a244028..4331ba8 100644 --- a/app/src/main/java/com/example/quxiang/opensource/OpenSourceRegisterActivity.kt +++ b/app/src/main/java/com/example/quxiang/opensource/OpenSourceRegisterActivity.kt @@ -1,8 +1,21 @@ package com.example.quxiang.opensource +import android.app.Activity +import android.content.Intent import android.view.KeyEvent import android.widget.TextView +import com.example.core.extension.logDebug +import com.example.core.extension.logWarn +import com.example.core.extension.showToast +import com.example.core.util.GlobalUtil import com.example.main.login.ui.RegisterActivity +import com.example.main.util.ResponseHandler +import com.example.quxiang.R +import com.quxianggif.network.model.BaseRegister +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.PhoneRegister +import com.quxianggif.network.model.Response +import java.lang.Exception /** * Anthor: Zhuangmingzhu @@ -11,12 +24,103 @@ import com.example.main.login.ui.RegisterActivity */ class OpenSourceRegisterActivity :RegisterActivity(),TextView.OnEditorActionListener{ - override fun forwardToMainActivity() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + private var number="" + + private var code="" + + //获取Intent中传递过来的数据并显示到界面上 + override fun setupViews() { + super.setupViews() + if(intent.getStringExtra(INTENT_PHONE_NUMBER)==null||intent.getStringExtra(INTENT_VERIFY_CODE) == null){ + showToast(GlobalUtil.getString(R.string.phone_number_verify_code_is_null)) + finish() + return + } + number=intent.getStringExtra(INTENT_PHONE_NUMBER) + code=intent.getStringExtra(INTENT_VERIFY_CODE) + nicknameEditText.requestFocus() + } + + //开始执行注册逻辑 + override fun doRegister() { + if(isRegistering) return + when(loginType){ + TYPE_PHONE_LOGIN->processPhoneRegister() + } + } + + //注册手机号登录账号 + private fun processPhoneRegister(){ + if(isNicknameValid){ + hideSoftKeyboard() + nicknameLayout.isErrorEnabled=false + registerInProgress(true) + sendPhoneRegisterRequest() + } + } + + private fun sendPhoneRegisterRequest(){ + PhoneRegister.getResponse(number,code,nicknameEditText.text.toString().trim(),object :Callback{ + override fun onResponse(response: Response) { + handleRegisterCallback(response) + } + + override fun onFailure(e: Exception) { + logWarn(TAG, e.message, e) + registerInProgress(false) + ResponseHandler.handleFailure(e) + } + + }) + } + + private fun handleRegisterCallback(response: Response){ + if(activity==null){ + return + } + if(!ResponseHandler.handleResponse(response)){ + val register=response as BaseRegister + val status=register.status + when(status){ + 0-> { + logDebug(TAG, "token is " + register.token + " , getAvatar is " + register.avatar) + val userId = register.userId + val token=register.token + saveAuthData(userId.toLong(),token,loginType) + registerSuccess() + } + 10105->{ + registerInProgress(false) + nicknameLayout.isErrorEnabled=true + nicknameLayout.error=GlobalUtil.getString(R.string.register_failed_nickname_is_used) + } + else->{ + logWarn(TAG, "Register failed. " + GlobalUtil.getResponseClue(status, register.msg)) + showToast(register.msg) + finish() + } + } + }else{ + finish() + } } - override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + //处理注册成功时的逻辑,包括数据缓存,界面跳转等 + private fun registerSuccess(){ + getUserBaseInfo() + } + + companion object{ + + private const val TAG="OpenSourceRegisterActivity" + + fun registerByPhone(activity:Activity,number:String,code:String){ + val intent= Intent(activity,OpenSourceRegisterActivity::class.java) + intent.putExtra(INTENT_PHONE_NUMBER,number) + intent.putExtra(INTENT_VERIFY_CODE, code) + intent.putExtra(INTENT_LOGIN_TYPE, TYPE_PHONE_LOGIN) + activity.startActivity(intent) + } } } \ No newline at end of file diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index c60ef4b..a89faed 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -6,4 +6,12 @@ + + + + + diff --git a/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt b/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt index 0f9b04c..9aad582 100644 --- a/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt +++ b/main/src/main/java/com/example/main/feeds/ui/MainActivity.kt @@ -2,13 +2,14 @@ package com.example.main.feeds.ui import android.app.Activity import android.content.Intent +import com.example.main.ui.BaseActivity /** * Anthor: Zhuangmingzhu * Date: 2019/4/11 下午6:27 * Describe: */ -class MainActivity { +class MainActivity :BaseActivity(){ companion object { diff --git a/main/src/main/java/com/example/main/login/ui/LoginActivity.kt b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt index e28fc14..6aee020 100644 --- a/main/src/main/java/com/example/main/login/ui/LoginActivity.kt +++ b/main/src/main/java/com/example/main/login/ui/LoginActivity.kt @@ -13,6 +13,11 @@ import com.example.core.extension.logError import com.example.core.model.Version import com.example.core.util.AndroidVersion import com.example.main.R +import com.example.main.event.FinishActivityEvent +import com.example.main.event.MessageEvent +import com.example.main.feeds.ui.MainActivity +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode /** * Anthor: Zhuangmingzhu @@ -64,5 +69,22 @@ abstract class LoginActivity :AuthActivity() { //是否进行动画 protected var isTransitioning=false + override fun onBackPressed() { + if(isTransitioning){ + finish() + } + } + + override fun forwardToMainActivity() { + MainActivity.actionStart(this) + finish() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + override fun onMessageEvent(messageEvent: MessageEvent) { + if (messageEvent is FinishActivityEvent && LoginActivity::class.java == messageEvent.activityClass) { + finish() + } + } } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/login/ui/RegisterActivity.kt b/main/src/main/java/com/example/main/login/ui/RegisterActivity.kt index 8feae90..e6ff08a 100644 --- a/main/src/main/java/com/example/main/login/ui/RegisterActivity.kt +++ b/main/src/main/java/com/example/main/login/ui/RegisterActivity.kt @@ -1,8 +1,21 @@ package com.example.main.login.ui +import android.os.Bundle import android.support.design.widget.TextInputLayout +import android.transition.Fade +import android.transition.TransitionManager +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.TextView +import com.example.core.util.AndroidVersion +import com.example.core.util.GlobalUtil +import com.example.main.R +import com.example.main.event.FinishActivityEvent +import com.example.main.feeds.ui.MainActivity +import kotlinx.android.synthetic.main.activity_register.* +import org.greenrobot.eventbus.EventBus /** * Anthor: Zhuangmingzhu @@ -23,6 +36,92 @@ abstract class RegisterActivity :AuthActivity(),TextView.OnEditorActionListener{ lateinit var nicknameLayout: TextInputLayout + //判断用户昵称是否合法。用户昵称长度必须在2-30个字符之间,并且只能包含中英文,数字,下划线和横线 + val isNicknameValid:Boolean + get() { + val nickname=nicknameEdit.text.toString() + if(nickname.length<2){ + nicknameInputLayout.isErrorEnabled = true + nicknameInputLayout.error = GlobalUtil.getString(R.string.nickname_length_invalid) + return false + }else if(!nickname.matches(NICK_NAME_REG_EXP.toRegex())){ + nicknameInputLayout.isErrorEnabled=true + nicknameInputLayout.error=GlobalUtil.getString(R.string.nickname_invalid) + return false + } + return true + } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_register) + } + + //获取Intent中传递过来的数据并显示到界面上 + override fun setupViews() { + setupToolbar() + nicknameEditText=nicknameEdit + nicknameLayout=nicknameInputLayout + title=""//注册界面的Toolbar不需要Title + moveOnText.setOnClickListener { doRegister() } + nicknameEdit.setOnEditorActionListener(this) + loginType=intent.getIntExtra(INTENT_LOGIN_TYPE,0) + } + + override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { + if(actionId==EditorInfo.IME_ACTION_GO){ + doRegister() + } + return false + } + + //根据用户是否正在注册来刷新页面。如果正在处理就显示进度条,否则的话就显示输入框 + protected fun registerInProgress(inProgress:Boolean){ + if(AndroidVersion.hasMarshmallow()){ + TransitionManager.beginDelayedTransition(registerRootLayout, Fade()) + } + isRegistering=inProgress + if(inProgress){ + moveOnText.visibility = View.GONE + progressBar.visibility = View.VISIBLE + nicknameInputLayout.visibility = View.GONE + }else{ + moveOnText.visibility = View.VISIBLE + progressBar.visibility = View.GONE + nicknameInputLayout.visibility = View.VISIBLE + } + } + + override fun forwardToMainActivity() { + MainActivity.actionStart(this) + val event=FinishActivityEvent() + event.activityClass=LoginActivity::class.java + EventBus.getDefault().post(event) + finish() + } + + //开始执行注册逻辑 + abstract fun doRegister() + + companion object{ + private const val TAG="RegisterActivity" + + const val INTENT_OPEN_ID = "intent_open_id" + + const val INTENT_ACCESS_TOKEN = "intent_access_token" + + const val INTENT_NICKNAME = "intent_nickname" + + const val INTENT_PHONE_NUMBER = "intent_phone_number" + + const val INTENT_VERIFY_CODE = "intent_verify_code" + + const val INTENT_LOGIN_TYPE = "intent_login_type" + + /** + * 检查用户昵称是否合法的正式表达式。 + */ + const val NICK_NAME_REG_EXP = "^[\u4E00-\u9FA5A-Za-z0-9_\\-]+$" + } } \ No newline at end of file diff --git a/main/src/main/res/drawable/move_on_bg.xml b/main/src/main/res/drawable/move_on_bg.xml new file mode 100644 index 0000000..58969da --- /dev/null +++ b/main/src/main/res/drawable/move_on_bg.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/activity_register.xml b/main/src/main/res/layout/activity_register.xml new file mode 100644 index 0000000..6ace3bd --- /dev/null +++ b/main/src/main/res/layout/activity_register.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values-v21/styles.xml b/main/src/main/res/values-v21/styles.xml index 01cb596..9378199 100644 --- a/main/src/main/res/values-v21/styles.xml +++ b/main/src/main/res/values-v21/styles.xml @@ -13,4 +13,8 @@ @null + + \ No newline at end of file diff --git a/main/src/main/res/values/styles.xml b/main/src/main/res/values/styles.xml index ba95bfb..409eaac 100644 --- a/main/src/main/res/values/styles.xml +++ b/main/src/main/res/values/styles.xml @@ -28,8 +28,12 @@ + + + \ No newline at end of file From 97dcd011ef6dc5ed9f235f9f69a5cd0e87aa8492 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Sun, 5 May 2019 14:05:34 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E4=B8=BB=E9=A1=B5adapter=E7=9A=84?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- .../main/common/view/CheckableImageButton.kt | 56 +++++++++ .../feeds/adapter/WaterFallFeedAdapter.kt | 25 +++- .../main/feeds/adapter/WorldFeedAdapter.kt | 9 ++ .../res/animator-v21/collapse_full_heart.xml | 19 +++ .../res/animator-v21/expand_full_heart.xml | 18 +++ .../res/animator-v21/fade_in_empty_heart.xml | 8 ++ .../res/animator-v21/fade_out_empty_heart.xml | 8 ++ .../main/res/drawable-v21/heart_anim_18dp.xml | 25 ++++ .../drawable-v21/heart_anim_reverse_18dp.xml | 26 ++++ .../res/drawable-v21/heart_empty_18dp.xml | 13 ++ .../main/res/drawable-v21/heart_fill_18dp.xml | 12 ++ .../main/res/drawable-v21/ic_heart_18dp.xml | 14 +++ .../res/drawable-v21/ic_heart_empty_18dp.xml | 13 ++ .../res/drawable-v21/ic_heart_full_18dp.xml | 12 ++ .../main/res/drawable-v22/ic_heart_18dp.xml | 24 ++++ .../main/res/layout-v21/world_feed_item.xml | 111 ++++++++++++++++++ main/src/main/res/layout/world_feed_item.xml | 104 ++++++++++++++++ .../main/res/values-v21/transition_svg.xml | 6 + 19 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 main/src/main/java/com/example/main/common/view/CheckableImageButton.kt create mode 100644 main/src/main/res/animator-v21/collapse_full_heart.xml create mode 100644 main/src/main/res/animator-v21/expand_full_heart.xml create mode 100644 main/src/main/res/animator-v21/fade_in_empty_heart.xml create mode 100644 main/src/main/res/animator-v21/fade_out_empty_heart.xml create mode 100644 main/src/main/res/drawable-v21/heart_anim_18dp.xml create mode 100644 main/src/main/res/drawable-v21/heart_anim_reverse_18dp.xml create mode 100644 main/src/main/res/drawable-v21/heart_empty_18dp.xml create mode 100644 main/src/main/res/drawable-v21/heart_fill_18dp.xml create mode 100644 main/src/main/res/drawable-v21/ic_heart_18dp.xml create mode 100644 main/src/main/res/drawable-v21/ic_heart_empty_18dp.xml create mode 100644 main/src/main/res/drawable-v21/ic_heart_full_18dp.xml create mode 100644 main/src/main/res/drawable-v22/ic_heart_18dp.xml create mode 100755 main/src/main/res/layout-v21/world_feed_item.xml create mode 100644 main/src/main/res/layout/world_feed_item.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a7509..7bfef59 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/main/src/main/java/com/example/main/common/view/CheckableImageButton.kt b/main/src/main/java/com/example/main/common/view/CheckableImageButton.kt new file mode 100644 index 0000000..ba25568 --- /dev/null +++ b/main/src/main/java/com/example/main/common/view/CheckableImageButton.kt @@ -0,0 +1,56 @@ +package com.example.main.common.view + +import android.content.Context +import android.util.AttributeSet +import android.view.SoundEffectConstants +import android.view.View +import android.widget.Checkable +import android.widget.ImageButton + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/5/5 上午10:46 + * Describe:An extension to [ImageButton] which implements the [Checkable] interface. + */ +class CheckableImageButton(context: Context,attrs:AttributeSet ) :ImageButton(context,attrs),Checkable{ + + private var isChecked=false + + override fun isChecked(): Boolean { + return isChecked + } + + override fun toggle() { + setChecked(!isChecked) + } + + override fun setChecked(checked: Boolean) { + if(isChecked!=checked){ + isChecked=checked + refreshDrawableState() + } + } + + override fun performClick(): Boolean { + toggle() + val handled=super.performClick() + if(!handled){ + playSoundEffect(SoundEffectConstants.CLICK) + } + return handled + } + + override fun onCreateDrawableState(extraSpace: Int): IntArray { + val drawableState=super.onCreateDrawableState(extraSpace+1) + if(isChecked()){ + View.mergeDrawableStates(drawableState, CHECKED_STATE_SET) + } + return drawableState + } + + companion object { + + private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked) + } + +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/feeds/adapter/WaterFallFeedAdapter.kt b/main/src/main/java/com/example/main/feeds/adapter/WaterFallFeedAdapter.kt index beadc8d..11600f8 100644 --- a/main/src/main/java/com/example/main/feeds/adapter/WaterFallFeedAdapter.kt +++ b/main/src/main/java/com/example/main/feeds/adapter/WaterFallFeedAdapter.kt @@ -1,10 +1,15 @@ package com.example.main.feeds.adapter import android.app.Activity +import android.support.v7.widget.CardView import android.support.v7.widget.RecyclerView import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView import com.example.core.model.WaterFallFeed +import com.example.main.R /** * Anthor: Zhuangmingzhu @@ -28,8 +33,26 @@ abstract class WaterFallFeedAdapter(protected var activity:Acti } } + abstract fun createFeedHolder(parent: ViewGroup): FeedViewHolder + + abstract fun bindFeedHolder(holder: FeedViewHolder, position: Int) + open class FeedViewHolder(view: View):RecyclerView.ViewHolder(view){ - + val cardView: CardView = view as CardView + + val feedCover: ImageView = view.findViewById(R.id.feedCover) + + val feedContent: TextView = view.findViewById(R.id.feedContent) + + val avatar: ImageView = view.findViewById(R.id.avatar) + + val nickname: TextView = view.findViewById(R.id.nickname) + + val likes: ImageView = view.findViewById(R.id.likes) + + val likesCount: TextView = view.findViewById(R.id.likesCount) + + val likesLayout: LinearLayout = view.findViewById(R.id.likesLayout) } companion object{ diff --git a/main/src/main/java/com/example/main/feeds/adapter/WorldFeedAdapter.kt b/main/src/main/java/com/example/main/feeds/adapter/WorldFeedAdapter.kt index bc3ea2a..272aeb6 100644 --- a/main/src/main/java/com/example/main/feeds/adapter/WorldFeedAdapter.kt +++ b/main/src/main/java/com/example/main/feeds/adapter/WorldFeedAdapter.kt @@ -11,4 +11,13 @@ import com.example.main.feeds.ui.WorldFeedsFragment */ class WorldFeedAdapter(private val fragment:WorldFeedsFragment, feedList:List, imageWidth:Int, layoutManager: RecyclerView.LayoutManager, override var isNoMoreData: Boolean, override var isLoadFailed: Boolean):WaterFallFeedAdapter(fragment.activity, feedList, imageWidth, layoutManager) { + + override fun getItemCount(): Int { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onBindViewHolder(p0: RecyclerView.ViewHolder, p1: Int) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + } \ No newline at end of file diff --git a/main/src/main/res/animator-v21/collapse_full_heart.xml b/main/src/main/res/animator-v21/collapse_full_heart.xml new file mode 100644 index 0000000..34d12d9 --- /dev/null +++ b/main/src/main/res/animator-v21/collapse_full_heart.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/animator-v21/expand_full_heart.xml b/main/src/main/res/animator-v21/expand_full_heart.xml new file mode 100644 index 0000000..149e7e7 --- /dev/null +++ b/main/src/main/res/animator-v21/expand_full_heart.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/animator-v21/fade_in_empty_heart.xml b/main/src/main/res/animator-v21/fade_in_empty_heart.xml new file mode 100644 index 0000000..25f57b0 --- /dev/null +++ b/main/src/main/res/animator-v21/fade_in_empty_heart.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/main/src/main/res/animator-v21/fade_out_empty_heart.xml b/main/src/main/res/animator-v21/fade_out_empty_heart.xml new file mode 100644 index 0000000..5b38d1e --- /dev/null +++ b/main/src/main/res/animator-v21/fade_out_empty_heart.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/heart_anim_18dp.xml b/main/src/main/res/drawable-v21/heart_anim_18dp.xml new file mode 100644 index 0000000..baeca6b --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_anim_18dp.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/heart_anim_reverse_18dp.xml b/main/src/main/res/drawable-v21/heart_anim_reverse_18dp.xml new file mode 100644 index 0000000..296931e --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_anim_reverse_18dp.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/heart_empty_18dp.xml b/main/src/main/res/drawable-v21/heart_empty_18dp.xml new file mode 100644 index 0000000..6315ad4 --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_empty_18dp.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/heart_fill_18dp.xml b/main/src/main/res/drawable-v21/heart_fill_18dp.xml new file mode 100644 index 0000000..0976dba --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_fill_18dp.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/ic_heart_18dp.xml b/main/src/main/res/drawable-v21/ic_heart_18dp.xml new file mode 100644 index 0000000..86b3d5f --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_18dp.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/ic_heart_empty_18dp.xml b/main/src/main/res/drawable-v21/ic_heart_empty_18dp.xml new file mode 100644 index 0000000..86cee69 --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_empty_18dp.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/ic_heart_full_18dp.xml b/main/src/main/res/drawable-v21/ic_heart_full_18dp.xml new file mode 100644 index 0000000..5656603 --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_full_18dp.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v22/ic_heart_18dp.xml b/main/src/main/res/drawable-v22/ic_heart_18dp.xml new file mode 100644 index 0000000..b488709 --- /dev/null +++ b/main/src/main/res/drawable-v22/ic_heart_18dp.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout-v21/world_feed_item.xml b/main/src/main/res/layout-v21/world_feed_item.xml new file mode 100755 index 0000000..87d1cad --- /dev/null +++ b/main/src/main/res/layout-v21/world_feed_item.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/layout/world_feed_item.xml b/main/src/main/res/layout/world_feed_item.xml new file mode 100644 index 0000000..306eabe --- /dev/null +++ b/main/src/main/res/layout/world_feed_item.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values-v21/transition_svg.xml b/main/src/main/res/values-v21/transition_svg.xml index c2c9d5f..bcb53cc 100644 --- a/main/src/main/res/values-v21/transition_svg.xml +++ b/main/src/main/res/values-v21/transition_svg.xml @@ -1,5 +1,11 @@ + + M16.05,5 C14.484,5 12.981,5.70626703 12,6.81798365 C11.019,5.70626703 9.516,5 7.95,5 C5.1735,5 3,7.10572207 3,9.79564033 C3,13.0871935 6.06,15.7771117 10.695,19.853406 L12,21 L13.305,19.853406 C17.94,15.7771117 21,13.0871935 21,9.79564033 C21,7.10572207 18.8265,5 16.05,5 L16.05,5 Z M12.0945,18.5629428 L12,18.6457766 L11.9055,18.5629428 C7.626,14.800545 4.8,12.3155313 4.8,9.79564033 C4.8,8.05613079 6.1545,6.74386921 7.95,6.74386921 C9.336,6.74386921 10.686,7.61144414 11.1585,8.80163488 L12.837,8.80163488 C13.314,7.61144414 14.664,6.74386921 16.05,6.74386921 C17.8455,6.74386921 19.2,8.05613079 19.2,9.79564033 C19.2,12.3155313 16.374,14.800545 12.0945,18.5629428 L12.0945,18.5629428 Z + M12,21 L10.695,19.853406 C6.06,15.7771117 3,13.0871935 3,9.79564033 C3,7.10572207 5.1735,5 7.95,5 C9.516,5 11.019,5.70626703 12,6.81798365 C12.981,5.70626703 14.484,5 16.05,5 C18.8265,5 21,7.10572207 21,9.79564033 C21,13.0871935 17.94,15.7771117 13.305,19.853406 L12,21 L12,21 Z + M13.3843083,13.3956843 C11.233862,15.5399983 7.7581039,15.5381046 5.61000013,13.3900003 C3.46000004,11.2399998 3.46000004,7.76000023 5.61000013,5.61000013 C7.76000023,3.46000004 11.2400007,3.46000004 13.3900003,5.61000013 C15.54,7.76000023 15.5400009,11.2400007 13.3900003,13.3900003 C13.388104,13.3918967 13.3862067,13.3937913 13.3843083,13.3956843 C15.1427975,15.1834093 19.6826174,19.798706 19.6826172,19.7987061 L13.3843085,13.3956846 L13.3843083,13.3956843 Z + + transition_logo_splash \ No newline at end of file From dad5a20194f7b343ec6f76c217fd3f4dabf7a9a2 Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Thu, 13 Jun 2019 10:35:35 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E7=9A=84=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 +- app/proguard-rules.pro | 3 + .../example/bumptech/glide/BitmapOptions.java | 9 + .../bumptech/glide/BitmapRequestBuilder.java | 511 ++++++++++ .../bumptech/glide/BitmapTypeRequest.java | 102 ++ .../example/bumptech/glide/BuildConfig.java | 13 + .../bumptech/glide/DownloadOptions.java | 30 + .../bumptech/glide/DrawableOptions.java | 65 ++ .../glide/DrawableRequestBuilder.java | 460 +++++++++ .../bumptech/glide/DrawableTypeRequest.java | 111 +++ .../bumptech/glide/GenericRequestBuilder.java | 861 +++++++++++++++++ .../glide/GenericTranscodeRequest.java | 111 +++ .../bumptech/glide/GifRequestBuilder.java | 434 +++++++++ .../bumptech/glide/GifTypeRequest.java | 81 ++ .../com/example/bumptech/glide/Glide.java | 714 ++++++++++++++ .../example/bumptech/glide/GlideBuilder.java | 192 ++++ .../example/bumptech/glide/ListPreloader.java | 317 +++++++ .../bumptech/glide/MemoryCategory.java | 33 + .../com/example/bumptech/glide/Priority.java | 13 + .../bumptech/glide/RequestManager.java | 804 ++++++++++++++++ .../glide/disklrucache/DiskLruCache.java | 875 ++++++++++++++++++ .../glide/disklrucache/StrictLineReader.java | 196 ++++ .../bumptech/glide/disklrucache/Util.java | 77 ++ .../glide/gifdecoder/BuildConfig.java | 13 + .../bumptech/glide/gifdecoder/GifDecoder.java | 710 ++++++++++++++ .../bumptech/glide/gifdecoder/GifFrame.java | 22 + .../bumptech/glide/gifdecoder/GifHeader.java | 57 ++ .../glide/gifdecoder/GifHeaderParser.java | 375 ++++++++ .../glide/gifencoder/AnimatedGifEncoder.java | 531 +++++++++++ .../glide/gifencoder/BuildConfig.java | 13 + .../bumptech/glide/gifencoder/LZWEncoder.java | 297 ++++++ .../bumptech/glide/gifencoder/NeuQuant.java | 506 ++++++++++ .../bumptech/glide/load/DecodeFormat.java | 49 + .../example/bumptech/glide/load/Encoder.java | 31 + .../com/example/bumptech/glide/load/Key.java | 31 + .../glide/load/MultiTransformation.java | 58 ++ .../bumptech/glide/load/ResourceDecoder.java | 50 + .../bumptech/glide/load/ResourceEncoder.java | 13 + .../bumptech/glide/load/Transformation.java | 47 + .../glide/load/data/AssetPathFetcher.java | 74 ++ .../glide/load/data/ByteArrayFetcher.java | 41 + .../bumptech/glide/load/data/DataFetcher.java | 77 ++ .../load/data/ExifOrientationStream.java | 133 +++ .../data/FileDescriptorAssetPathFetcher.java | 25 + .../data/FileDescriptorLocalUriFetcher.java | 28 + .../glide/load/data/HttpUrlFetcher.java | 145 +++ .../glide/load/data/LocalUriFetcher.java | 94 ++ .../load/data/MediaStoreThumbFetcher.java | 262 ++++++ .../load/data/StreamAssetPathFetcher.java | 25 + .../load/data/StreamLocalUriFetcher.java | 28 + .../glide/load/engine/CacheLoader.java | 43 + .../bumptech/glide/load/engine/DecodeJob.java | 298 ++++++ .../glide/load/engine/DiskCacheStrategy.java | 37 + .../bumptech/glide/load/engine/Engine.java | 383 ++++++++ .../bumptech/glide/load/engine/EngineJob.java | 213 +++++ .../glide/load/engine/EngineJobListener.java | 11 + .../bumptech/glide/load/engine/EngineKey.java | 176 ++++ .../glide/load/engine/EngineKeyFactory.java | 21 + .../glide/load/engine/EngineResource.java | 104 +++ .../glide/load/engine/EngineRunnable.java | 141 +++ .../glide/load/engine/OriginalKey.java | 55 ++ .../bumptech/glide/load/engine/Resource.java | 48 + .../glide/load/engine/ResourceRecycler.java | 44 + .../bitmap_recycle/AttributeStrategy.java | 122 +++ .../engine/bitmap_recycle/BaseKeyPool.java | 27 + .../engine/bitmap_recycle/BitmapPool.java | 114 +++ .../bitmap_recycle/BitmapPoolAdapter.java | 44 + .../bitmap_recycle/GroupedLinkedMap.java | 146 +++ .../engine/bitmap_recycle/LruBitmapPool.java | 271 ++++++ .../bitmap_recycle/LruPoolStrategy.java | 12 + .../load/engine/bitmap_recycle/Poolable.java | 5 + .../bitmap_recycle/PrettyPrintTreeMap.java | 18 + .../bitmap_recycle/SizeConfigStrategy.java | 239 +++++ .../engine/bitmap_recycle/SizeStrategy.java | 158 ++++ .../glide/load/engine/cache/DiskCache.java | 76 ++ .../load/engine/cache/DiskCacheAdapter.java | 32 + .../engine/cache/DiskCacheWriteLocker.java | 91 ++ .../engine/cache/DiskLruCacheFactory.java | 74 ++ .../engine/cache/DiskLruCacheWrapper.java | 138 +++ .../cache/ExternalCacheDiskCacheFactory.java | 38 + .../cache/InternalCacheDiskCacheFactory.java | 36 + .../load/engine/cache/LruResourceCache.java | 55 ++ .../glide/load/engine/cache/MemoryCache.java | 74 ++ .../load/engine/cache/MemoryCacheAdapter.java | 54 ++ .../engine/cache/MemorySizeCalculator.java | 119 +++ .../load/engine/cache/SafeKeyGenerator.java | 52 ++ .../FifoPriorityThreadPoolExecutor.java | 167 ++++ .../load/engine/executor/Prioritized.java | 12 + .../engine/prefill/BitmapPreFillRunner.java | 162 ++++ .../load/engine/prefill/BitmapPreFiller.java | 83 ++ .../load/engine/prefill/PreFillQueue.java | 49 + .../load/engine/prefill/PreFillType.java | 172 ++++ .../glide/load/model/AssetUriParser.java | 36 + .../bumptech/glide/load/model/FileLoader.java | 28 + .../load/model/GenericLoaderFactory.java | 202 ++++ .../bumptech/glide/load/model/GlideUrl.java | 146 +++ .../bumptech/glide/load/model/Headers.java | 33 + .../load/model/ImageVideoModelLoader.java | 127 +++ .../glide/load/model/ImageVideoWrapper.java | 26 + .../load/model/ImageVideoWrapperEncoder.java | 43 + .../glide/load/model/LazyHeaderFactory.java | 13 + .../glide/load/model/LazyHeaders.java | 263 ++++++ .../bumptech/glide/load/model/ModelCache.java | 112 +++ .../glide/load/model/ModelLoader.java | 50 + .../glide/load/model/ModelLoaderFactory.java | 31 + .../glide/load/model/ResourceLoader.java | 53 ++ .../glide/load/model/StreamEncoder.java | 42 + .../glide/load/model/StringLoader.java | 45 + .../bumptech/glide/load/model/UriLoader.java | 53 ++ .../bumptech/glide/load/model/UrlLoader.java | 26 + .../FileDescriptorFileLoader.java | 45 + .../FileDescriptorModelLoader.java | 15 + .../FileDescriptorResourceLoader.java | 44 + .../FileDescriptorStringLoader.java | 43 + .../FileDescriptorUriLoader.java | 56 ++ .../load/model/stream/BaseGlideUrlLoader.java | 87 ++ .../model/stream/HttpUrlGlideUrlLoader.java | 62 ++ .../model/stream/MediaStoreStreamLoader.java | 35 + .../model/stream/StreamByteArrayLoader.java | 54 ++ .../load/model/stream/StreamFileLoader.java | 44 + .../load/model/stream/StreamModelLoader.java | 15 + .../model/stream/StreamResourceLoader.java | 43 + .../load/model/stream/StreamStringLoader.java | 43 + .../load/model/stream/StreamUriLoader.java | 59 ++ .../load/model/stream/StreamUrlLoader.java | 40 + .../glide/load/resource/NullDecoder.java | 36 + .../glide/load/resource/NullEncoder.java | 36 + .../load/resource/NullResourceEncoder.java | 36 + .../glide/load/resource/SimpleResource.java | 37 + .../load/resource/UnitTransformation.java | 34 + .../load/resource/bitmap/BitmapDecoder.java | 40 + .../bitmap/BitmapDrawableResource.java | 36 + .../load/resource/bitmap/BitmapEncoder.java | 70 ++ .../load/resource/bitmap/BitmapResource.java | 59 ++ .../resource/bitmap/BitmapTransformation.java | 95 ++ .../load/resource/bitmap/CenterCrop.java | 42 + .../load/resource/bitmap/Downsampler.java | 391 ++++++++ .../FileDescriptorBitmapDataLoadProvider.java | 54 ++ .../bitmap/FileDescriptorBitmapDecoder.java | 54 ++ .../glide/load/resource/bitmap/FitCenter.java | 34 + .../resource/bitmap/GlideBitmapDrawable.java | 193 ++++ .../bitmap/GlideBitmapDrawableResource.java | 27 + .../resource/bitmap/ImageHeaderParser.java | 382 ++++++++ .../bitmap/ImageVideoBitmapDecoder.java | 61 ++ .../bitmap/ImageVideoDataLoadProvider.java | 56 ++ .../bitmap/RecyclableBufferedInputStream.java | 416 +++++++++ .../bitmap/StreamBitmapDataLoadProvider.java | 54 ++ .../resource/bitmap/StreamBitmapDecoder.java | 66 ++ .../resource/bitmap/TransformationUtils.java | 319 +++++++ .../resource/bitmap/VideoBitmapDecoder.java | 77 ++ .../load/resource/bytes/BytesResource.java | 33 + .../resource/drawable/DrawableResource.java | 35 + .../load/resource/drawable/GlideDrawable.java | 29 + .../glide/load/resource/file/FileDecoder.java | 23 + .../load/resource/file/FileResource.java | 15 + .../resource/file/FileToStreamDecoder.java | 64 ++ .../file/StreamFileDataLoadProvider.java | 63 ++ .../load/resource/gif/GifBitmapProvider.java | 28 + .../glide/load/resource/gif/GifDrawable.java | 377 ++++++++ .../resource/gif/GifDrawableLoadProvider.java | 53 ++ .../resource/gif/GifDrawableResource.java | 25 + .../gif/GifDrawableTransformation.java | 47 + .../load/resource/gif/GifFrameLoader.java | 220 +++++ .../resource/gif/GifFrameModelLoader.java | 43 + .../resource/gif/GifFrameResourceDecoder.java | 29 + .../load/resource/gif/GifResourceDecoder.java | 152 +++ .../load/resource/gif/GifResourceEncoder.java | 149 +++ .../resource/gifbitmap/GifBitmapWrapper.java | 52 ++ .../gifbitmap/GifBitmapWrapperResource.java | 43 + .../GifBitmapWrapperResourceDecoder.java | 151 +++ .../GifBitmapWrapperResourceEncoder.java | 46 + ...GifBitmapWrapperStreamResourceDecoder.java | 32 + .../GifBitmapWrapperTransformation.java | 54 ++ .../ImageVideoGifDrawableLoadProvider.java | 64 ++ .../transcode/BitmapBytesTranscoder.java | 41 + .../BitmapToGlideDrawableTranscoder.java | 40 + .../GifBitmapWrapperDrawableTranscoder.java | 44 + .../transcode/GifDrawableBytesTranscoder.java | 24 + .../GlideBitmapDrawableTranscoder.java | 41 + .../transcode/ResourceTranscoder.java | 22 + .../transcode/TranscoderRegistry.java | 59 ++ .../resource/transcode/UnitTranscoder.java | 28 + .../manager/ActivityFragmentLifecycle.java | 67 ++ .../glide/manager/ApplicationLifecycle.java | 17 + .../glide/manager/ConnectivityMonitor.java | 19 + .../manager/ConnectivityMonitorFactory.java | 21 + .../manager/DefaultConnectivityMonitor.java | 73 ++ .../manager/EmptyRequestManagerTreeNode.java | 17 + .../bumptech/glide/manager/Lifecycle.java | 11 + .../glide/manager/LifecycleListener.java | 23 + .../manager/NullConnectivityMonitor.java | 22 + .../glide/manager/RequestManagerFragment.java | 184 ++++ .../manager/RequestManagerRetriever.java | 229 +++++ .../glide/manager/RequestManagerTreeNode.java | 18 + .../glide/manager/RequestTracker.java | 116 +++ .../SupportRequestManagerFragment.java | 174 ++++ .../bumptech/glide/module/GlideModule.java | 98 ++ .../bumptech/glide/module/ManifestParser.java | 63 ++ .../glide/provider/ChildLoadProvider.java | 155 ++++ .../glide/provider/DataLoadProvider.java | 39 + .../provider/DataLoadProviderRegistry.java | 54 ++ .../glide/provider/EmptyDataLoadProvider.java | 43 + .../glide/provider/FixedLoadProvider.java | 92 ++ .../bumptech/glide/provider/LoadProvider.java | 29 + .../bumptech/glide/request/FutureTarget.java | 36 + .../glide/request/GenericRequest.java | 556 +++++++++++ .../bumptech/glide/request/Request.java | 59 ++ .../glide/request/RequestCoordinator.java | 33 + .../glide/request/RequestFutureTarget.java | 246 +++++ .../glide/request/RequestListener.java | 62 ++ .../glide/request/ResourceCallback.java | 25 + .../request/ThumbnailRequestCoordinator.java | 156 ++++ .../animation/DrawableCrossFadeFactory.java | 94 ++ .../DrawableCrossFadeViewAnimation.java | 56 ++ .../request/animation/GlideAnimation.java | 55 ++ .../animation/GlideAnimationFactory.java | 17 + .../glide/request/animation/NoAnimation.java | 47 + .../request/animation/ViewAnimation.java | 49 + .../animation/ViewAnimationFactory.java | 78 ++ .../animation/ViewPropertyAnimation.java | 57 ++ .../ViewPropertyAnimationFactory.java | 34 + .../glide/request/target/AppWidgetTarget.java | 137 +++ .../glide/request/target/BaseTarget.java | 93 ++ .../request/target/BitmapImageViewTarget.java | 27 + .../target/DrawableImageViewTarget.java | 18 + .../target/GlideDrawableImageViewTarget.java | 97 ++ .../glide/request/target/ImageViewTarget.java | 84 ++ .../target/ImageViewTargetFactory.java | 28 + .../request/target/NotificationTarget.java | 91 ++ .../glide/request/target/PreloadTarget.java | 34 + .../glide/request/target/SimpleTarget.java | 64 ++ .../request/target/SizeReadyCallback.java | 17 + .../request/target/SquaringDrawable.java | 237 +++++ .../bumptech/glide/request/target/Target.java | 97 ++ .../glide/request/target/ViewTarget.java | 299 ++++++ .../ApplicationVersionSignature.java | 63 ++ .../glide/signature/EmptySignature.java | 27 + .../glide/signature/MediaStoreSignature.java | 76 ++ .../glide/signature/StringSignature.java | 52 ++ .../bumptech/glide/util/ByteArrayPool.java | 77 ++ .../glide/util/ContentLengthInputStream.java | 78 ++ .../util/ExceptionCatchingInputStream.java | 132 +++ .../glide/util/FixedPreloadSizeProvider.java | 31 + .../example/bumptech/glide/util/LogTime.java | 39 + .../example/bumptech/glide/util/LruCache.java | 169 ++++ .../glide/util/MarkEnforcingInputStream.java | 87 ++ .../bumptech/glide/util/MultiClassKey.java | 58 ++ .../com/example/bumptech/glide/util/Util.java | 185 ++++ .../glide/util/ViewPreloadSizeProvider.java | 87 ++ .../com/example/core/extension/Drawable.kt | 47 + .../java/com/example/core/extension/Model.kt | 55 ++ .../java/com/example/core/model/HotFeed.kt | 14 + .../java/com/example/core/util/GlobalUtil.kt | 23 + .../transformations/BlurTransformation.java | 110 +++ .../CropCircleTransformation.java | 80 ++ .../transformations/internal/FastBlur.java | 257 +++++ .../transformations/internal/RSBlur.java | 66 ++ main/src/main/assets/litepal.xml | 8 +- .../main/comments/ui/CommentsActivity.kt | 30 + .../common/holder/LoadingMoreViewHolder.kt | 32 + .../common/transitions/TransitionUtils.kt | 93 ++ .../com/example/main/event/DeleteFeedEvent.kt | 19 + .../com/example/main/event/LikeFeedEvent.kt | 32 + .../example/main/event/ModifyUserInfoEvent.kt | 17 + .../main/feeds/adapter/HotFeedAdapter.kt | 62 ++ .../feeds/adapter/WaterFallFeedAdapter.kt | 231 +++++ .../main/feeds/adapter/WorldFeedAdapter.kt | 32 +- .../com/example/main/feeds/model/Draft.kt | 19 + .../main/feeds/ui/BaseFeedsFragment.kt | 10 +- .../example/main/feeds/ui/DraftActivity.kt | 24 + .../main/feeds/ui/FeedDetailActivity.kt | 49 + .../example/main/feeds/ui/HotFeedsFragment.kt | 31 + .../com/example/main/feeds/ui/MainActivity.kt | 250 ++++- .../example/main/feeds/ui/PostFeedActivity.kt | 45 + .../main/feeds/ui/WorldFeedsFragment.kt | 204 +++- .../main/feeds/view/SpaceItemDecoration.kt | 33 + .../main/settings/ui/SettingsActivity.kt | 35 + .../main/user/ui/ModifyUserInfoActivity.kt | 46 + .../user/ui/RecommendFollowingActivity.kt | 21 + .../main/user/ui/UserHomePageActivity.kt | 51 + .../java/com/example/main/util/AnimUtils.kt | 268 ++++++ .../java/com/example/main/util/ColorUtils.kt | 115 +++ .../main/java/com/example/main/util/OSUtil.kt | 100 ++ .../java/com/example/main/util/ViewUtils.kt | 126 +++ .../com/example/main/util/glide/CustomUrl.kt | 31 + .../res/drawable-hdpi/loading_bg_circle.xml | 5 + .../res/drawable-hdpi/loading_bg_rect.xml | 5 + .../src/main/res/layout-v21/hot_feed_item.xml | 138 +++ main/src/main/res/layout/hot_feed_item.xml | 138 +++ main/src/main/res/layout/loading_footer.xml | 29 + .../transition-v21/compose_fab_reenter.xml | 28 + .../main/res/values-v21/transition_svg.xml | 3 + .../network/model/FetchWorldFeeds.kt | 28 + .../com/quxianggif/network/model/LikeFeed.kt | 19 + .../network/request/FetchWorldFeedsRequest.kt | 57 ++ .../network/request/LikeFeedRequest.kt | 54 ++ 296 files changed, 29658 insertions(+), 28 deletions(-) create mode 100755 core/src/main/java/com/example/bumptech/glide/BitmapOptions.java create mode 100755 core/src/main/java/com/example/bumptech/glide/BitmapRequestBuilder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/BitmapTypeRequest.java create mode 100755 core/src/main/java/com/example/bumptech/glide/BuildConfig.java create mode 100755 core/src/main/java/com/example/bumptech/glide/DownloadOptions.java create mode 100755 core/src/main/java/com/example/bumptech/glide/DrawableOptions.java create mode 100755 core/src/main/java/com/example/bumptech/glide/DrawableRequestBuilder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/DrawableTypeRequest.java create mode 100755 core/src/main/java/com/example/bumptech/glide/GenericRequestBuilder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/GenericTranscodeRequest.java create mode 100755 core/src/main/java/com/example/bumptech/glide/GifRequestBuilder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/GifTypeRequest.java create mode 100755 core/src/main/java/com/example/bumptech/glide/Glide.java create mode 100755 core/src/main/java/com/example/bumptech/glide/GlideBuilder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/ListPreloader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/MemoryCategory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/Priority.java create mode 100755 core/src/main/java/com/example/bumptech/glide/RequestManager.java create mode 100755 core/src/main/java/com/example/bumptech/glide/disklrucache/DiskLruCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/disklrucache/StrictLineReader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/disklrucache/Util.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifdecoder/BuildConfig.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifdecoder/GifDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifdecoder/GifFrame.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeaderParser.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifencoder/AnimatedGifEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifencoder/BuildConfig.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifencoder/LZWEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/gifencoder/NeuQuant.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/DecodeFormat.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/Encoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/Key.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/MultiTransformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/ResourceDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/ResourceEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/Transformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/AssetPathFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/ByteArrayFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/DataFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/ExifOrientationStream.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorAssetPathFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorLocalUriFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/HttpUrlFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/LocalUriFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/MediaStoreThumbFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/StreamAssetPathFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/data/StreamLocalUriFetcher.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/CacheLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/DecodeJob.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/DiskCacheStrategy.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/Engine.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineJob.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineJobListener.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineKey.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineKeyFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/EngineRunnable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/OriginalKey.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/Resource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/ResourceRecycler.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategy.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BaseKeyPool.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPool.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPoolAdapter.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMap.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPool.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruPoolStrategy.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/Poolable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/PrettyPrintTreeMap.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeStrategy.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheAdapter.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheWriteLocker.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheWrapper.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/ExternalCacheDiskCacheFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/InternalCacheDiskCacheFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/LruResourceCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCacheAdapter.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemorySizeCalculator.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/cache/SafeKeyGenerator.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/executor/FifoPriorityThreadPoolExecutor.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/executor/Prioritized.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFiller.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillQueue.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillType.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/AssetUriParser.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/FileLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/GenericLoaderFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/GlideUrl.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/Headers.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoModelLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapper.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapperEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaderFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaders.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ModelCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ModelLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ModelLoaderFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/ResourceLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/StreamEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/StringLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/UriLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/UrlLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorFileLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorModelLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorResourceLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorStringLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorUriLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/BaseGlideUrlLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/MediaStoreStreamLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamByteArrayLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamFileLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamModelLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamResourceLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamStringLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUriLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUrlLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/NullDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/NullEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/NullResourceEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/SimpleResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/UnitTransformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDrawableResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapTransformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/CenterCrop.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/Downsampler.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FitCenter.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawableResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageHeaderParser.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoBitmapDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoDataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStream.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/TransformationUtils.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/bytes/BytesResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/drawable/DrawableResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/drawable/GlideDrawable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/file/FileDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/file/FileResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/file/FileToStreamDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/file/StreamFileDataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifBitmapProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableTransformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameModelLoader.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapper.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResource.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceEncoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperStreamResourceDecoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperTransformation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/ImageVideoGifDrawableLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapBytesTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapToGlideDrawableTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifBitmapWrapperDrawableTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GlideBitmapDrawableTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/ResourceTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/TranscoderRegistry.java create mode 100755 core/src/main/java/com/example/bumptech/glide/load/resource/transcode/UnitTranscoder.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/ActivityFragmentLifecycle.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/ApplicationLifecycle.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitor.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitorFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/DefaultConnectivityMonitor.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/EmptyRequestManagerTreeNode.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/Lifecycle.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/LifecycleListener.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/NullConnectivityMonitor.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/RequestManagerFragment.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/RequestManagerRetriever.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/RequestManagerTreeNode.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/RequestTracker.java create mode 100755 core/src/main/java/com/example/bumptech/glide/manager/SupportRequestManagerFragment.java create mode 100755 core/src/main/java/com/example/bumptech/glide/module/GlideModule.java create mode 100755 core/src/main/java/com/example/bumptech/glide/module/ManifestParser.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/ChildLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/DataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/DataLoadProviderRegistry.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/EmptyDataLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/FixedLoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/provider/LoadProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/FutureTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/GenericRequest.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/Request.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/RequestCoordinator.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/RequestFutureTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/RequestListener.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/ResourceCallback.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/ThumbnailRequestCoordinator.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeViewAnimation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimationFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/NoAnimation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimationFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimation.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimationFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/AppWidgetTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/BaseTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/BitmapImageViewTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/DrawableImageViewTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/GlideDrawableImageViewTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTargetFactory.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/NotificationTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/PreloadTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/SimpleTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/SizeReadyCallback.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/SquaringDrawable.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/Target.java create mode 100755 core/src/main/java/com/example/bumptech/glide/request/target/ViewTarget.java create mode 100755 core/src/main/java/com/example/bumptech/glide/signature/ApplicationVersionSignature.java create mode 100755 core/src/main/java/com/example/bumptech/glide/signature/EmptySignature.java create mode 100755 core/src/main/java/com/example/bumptech/glide/signature/MediaStoreSignature.java create mode 100755 core/src/main/java/com/example/bumptech/glide/signature/StringSignature.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/ByteArrayPool.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/ContentLengthInputStream.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/ExceptionCatchingInputStream.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/FixedPreloadSizeProvider.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/LogTime.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/LruCache.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/MarkEnforcingInputStream.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/MultiClassKey.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/Util.java create mode 100755 core/src/main/java/com/example/bumptech/glide/util/ViewPreloadSizeProvider.java create mode 100644 core/src/main/java/com/example/core/extension/Drawable.kt create mode 100644 core/src/main/java/com/example/core/extension/Model.kt create mode 100644 core/src/main/java/com/example/core/model/HotFeed.kt create mode 100755 core/src/main/java/jp/wasbeef/glide/transformations/BlurTransformation.java create mode 100755 core/src/main/java/jp/wasbeef/glide/transformations/CropCircleTransformation.java create mode 100755 core/src/main/java/jp/wasbeef/glide/transformations/internal/FastBlur.java create mode 100755 core/src/main/java/jp/wasbeef/glide/transformations/internal/RSBlur.java create mode 100644 main/src/main/java/com/example/main/comments/ui/CommentsActivity.kt create mode 100644 main/src/main/java/com/example/main/common/holder/LoadingMoreViewHolder.kt create mode 100644 main/src/main/java/com/example/main/common/transitions/TransitionUtils.kt create mode 100644 main/src/main/java/com/example/main/event/DeleteFeedEvent.kt create mode 100644 main/src/main/java/com/example/main/event/LikeFeedEvent.kt create mode 100644 main/src/main/java/com/example/main/event/ModifyUserInfoEvent.kt create mode 100644 main/src/main/java/com/example/main/feeds/adapter/HotFeedAdapter.kt create mode 100644 main/src/main/java/com/example/main/feeds/model/Draft.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/DraftActivity.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/FeedDetailActivity.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/HotFeedsFragment.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/PostFeedActivity.kt create mode 100644 main/src/main/java/com/example/main/feeds/view/SpaceItemDecoration.kt create mode 100644 main/src/main/java/com/example/main/settings/ui/SettingsActivity.kt create mode 100644 main/src/main/java/com/example/main/user/ui/ModifyUserInfoActivity.kt create mode 100644 main/src/main/java/com/example/main/user/ui/RecommendFollowingActivity.kt create mode 100644 main/src/main/java/com/example/main/user/ui/UserHomePageActivity.kt create mode 100644 main/src/main/java/com/example/main/util/AnimUtils.kt create mode 100644 main/src/main/java/com/example/main/util/ColorUtils.kt create mode 100644 main/src/main/java/com/example/main/util/OSUtil.kt create mode 100644 main/src/main/java/com/example/main/util/ViewUtils.kt create mode 100644 main/src/main/java/com/example/main/util/glide/CustomUrl.kt create mode 100644 main/src/main/res/drawable-hdpi/loading_bg_circle.xml create mode 100644 main/src/main/res/drawable-hdpi/loading_bg_rect.xml create mode 100755 main/src/main/res/layout-v21/hot_feed_item.xml create mode 100755 main/src/main/res/layout/hot_feed_item.xml create mode 100644 main/src/main/res/layout/loading_footer.xml create mode 100755 main/src/main/res/transition-v21/compose_fab_reenter.xml create mode 100644 network/src/main/java/com/quxianggif/network/model/FetchWorldFeeds.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/LikeFeed.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/FetchWorldFeedsRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/LikeFeedRequest.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..37a7509 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b4245..e90531f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-libraryjars libs/litepal-1.5.1.jar +-dontwarn org.litepal.** +-keep class org.litepal.** {*; } diff --git a/core/src/main/java/com/example/bumptech/glide/BitmapOptions.java b/core/src/main/java/com/example/bumptech/glide/BitmapOptions.java new file mode 100755 index 0000000..33a26a3 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/BitmapOptions.java @@ -0,0 +1,9 @@ +package com.example.bumptech.glide; + +interface BitmapOptions { + + GenericRequestBuilder fitCenter(); + + GenericRequestBuilder centerCrop(); + +} diff --git a/core/src/main/java/com/example/bumptech/glide/BitmapRequestBuilder.java b/core/src/main/java/com/example/bumptech/glide/BitmapRequestBuilder.java new file mode 100755 index 0000000..5d14fc9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/BitmapRequestBuilder.java @@ -0,0 +1,511 @@ +package com.example.bumptech.glide; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.ParcelFileDescriptor; +import android.view.animation.Animation; +import android.widget.ImageView; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.example.bumptech.glide.load.resource.bitmap.Downsampler; +import com.example.bumptech.glide.load.resource.bitmap.FileDescriptorBitmapDecoder; +import com.example.bumptech.glide.load.resource.bitmap.ImageVideoBitmapDecoder; +import com.example.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; +import com.example.bumptech.glide.load.resource.bitmap.VideoBitmapDecoder; +import com.example.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.RequestListener; +import com.example.bumptech.glide.request.animation.ViewPropertyAnimation; +import com.example.bumptech.glide.request.target.Target; + +import java.io.File; +import java.io.InputStream; + +/** + * A class for creating a request to load a bitmap for an image or from a video. Sets a variety of type independent + * options including resizing, animations, and placeholders. + * + *

+ * Warning - It is not safe to use this builder after calling into(), it may be pooled and + * reused. + *

+ * + * @param The type of model that will be loaded into the target. + * @param The type of the transcoded resource that the target will receive + */ +public class BitmapRequestBuilder + extends GenericRequestBuilder implements BitmapOptions { + private final BitmapPool bitmapPool; + + private Downsampler downsampler = Downsampler.AT_LEAST; + private DecodeFormat decodeFormat; + private ResourceDecoder imageDecoder; + private ResourceDecoder videoDecoder; + + BitmapRequestBuilder(LoadProvider loadProvider, + Class transcodeClass, GenericRequestBuilder other) { + super(loadProvider, transcodeClass, other); + this.bitmapPool = other.glide.getBitmapPool(); + this.decodeFormat = other.glide.getDecodeFormat(); + + imageDecoder = new StreamBitmapDecoder(bitmapPool, decodeFormat); + videoDecoder = new FileDescriptorBitmapDecoder(bitmapPool, decodeFormat); + } + + /** + * Load images at a size near the size of the target using {@link Downsampler#AT_LEAST}. + * + * @see #downsample(Downsampler) + * + * @return This request builder. + */ + public BitmapRequestBuilder approximate() { + return downsample(Downsampler.AT_LEAST); + } + + /** + * Load images at their original size using {@link Downsampler#NONE}. + * + * @see #downsample(Downsampler) + * + * @return This request builder. + */ + public BitmapRequestBuilder asIs() { + return downsample(Downsampler.NONE); + } + + /** + * Load images at a size that is at most exactly as big as the target using + * {@link Downsampler#AT_MOST}. + * + * @see #downsample(Downsampler) + * + * @return This request builder. + */ + public BitmapRequestBuilder atMost() { + return downsample(Downsampler.AT_MOST); + } + + /** + * Load images using the given {@link Downsampler}. Replaces any existing image decoder. Defaults to + * {@link Downsampler#AT_LEAST}. Will be ignored if the data represented by the model is a video. This replaces any + * previous calls to {@link #imageDecoder(ResourceDecoder)} and {@link #decoder(ResourceDecoder)} with default + * decoders with the appropriate options set. + * + * @see #imageDecoder + * + * @param downsampler The downsampler. + * @return This request builder. + */ + private BitmapRequestBuilder downsample(Downsampler downsampler) { + this.downsampler = downsampler; + imageDecoder = new StreamBitmapDecoder(downsampler, bitmapPool, decodeFormat); + super.decoder(new ImageVideoBitmapDecoder(imageDecoder, videoDecoder)); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder thumbnail(float sizeMultiplier) { + super.thumbnail(sizeMultiplier); + return this; + } + + /** + * Loads and displays the {@link Bitmap} retrieved by the given thumbnail request if it finishes + * before this request. Best used for loading thumbnail {@link Bitmap}s that are smaller and will be loaded more + * quickly than the fullsize {@link Bitmap}. There are no guarantees about the order in which the requests will + * actually finish. However, if the thumb request completes after the full request, the thumb + * {@link Bitmap} will never replace the full image. + * + * @see #thumbnail(float) + * + *

+ * Note - Any options on the main request will not be passed on to the thumbnail request. For example, if + * you want an animation to occur when either the full {@link Bitmap} loads or the thumbnail + * loads, you need to call {@link #animate(int)} on both the thumb and the full request. For a simpler thumbnail + * option where these options are applied to the humbnail as well, see {@link #thumbnail(float)}. + *

+ * + *

+ * Only the thumbnail call on the main request will be obeyed, recursive calls to this method are ignored. + *

+ * + * @param thumbnailRequest The request to use to load the thumbnail. + * @return This request builder. + */ + public BitmapRequestBuilder thumbnail(BitmapRequestBuilder + thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder sizeMultiplier(float sizeMultiplier) { + super.sizeMultiplier(sizeMultiplier); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder decoder(ResourceDecoder decoder) { + super.decoder(decoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder cacheDecoder(ResourceDecoder cacheDecoder) { + super.cacheDecoder(cacheDecoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder encoder(ResourceEncoder encoder) { + super.encoder(encoder); + return this; + } + + /** + * Sets the {@link ResourceDecoder} that will be used to decode {@link Bitmap}s obtained + * from an {@link InputStream}. + * + * @see #videoDecoder + * + * @param decoder The decoder to use to decode {@link Bitmap}s. + * @return This request builder. + */ + public BitmapRequestBuilder imageDecoder(ResourceDecoder decoder) { + imageDecoder = decoder; + super.decoder(new ImageVideoBitmapDecoder(decoder, videoDecoder)); + return this; + } + + /** + * Sets the {@link ResourceDecoder} that will be used to decode {@link Bitmap}s obtained + * from an {@link ParcelFileDescriptor}. + * + * @param decoder The decoder to use to decode {@link Bitmap}s. + * @return This request builder. + */ + public BitmapRequestBuilder videoDecoder( + ResourceDecoder decoder) { + videoDecoder = decoder; + super.decoder(new ImageVideoBitmapDecoder(imageDecoder, decoder)); + return this; + } + + /** + * Sets the preferred format for {@link Bitmap}s decoded in this request. Defaults to + * {@link DecodeFormat#PREFER_RGB_565}. This replaces any previous calls to {@link #imageDecoder(ResourceDecoder)}, + * {@link #videoDecoder(ResourceDecoder)}, {@link #decoder(ResourceDecoder)} and + * {@link #cacheDecoder(ResourceDecoder)}} with default decoders with the appropriate + * options set. + * + *

+ * Note - If using a {@link Transformation} that expect bitmaps to support transparency, this should always be + * set to ALWAYS_ARGB_8888. RGB_565 requires fewer bytes per pixel and is generally preferable, but it does not + * support transparency. + *

+ * + * @see DecodeFormat + * + * @param format The format to use. + * @return This request builder. + */ + public BitmapRequestBuilder format(DecodeFormat format) { + this.decodeFormat = format; + imageDecoder = new StreamBitmapDecoder(downsampler, bitmapPool, format); + videoDecoder = new FileDescriptorBitmapDecoder(new VideoBitmapDecoder(), bitmapPool, format); + super.cacheDecoder(new FileToStreamDecoder(new StreamBitmapDecoder(downsampler, bitmapPool, format))); + super.decoder(new ImageVideoBitmapDecoder(imageDecoder, videoDecoder)); + return this; + } + + @Override + public BitmapRequestBuilder priority(Priority priority) { + super.priority(priority); + return this; + } + + /** + * Transform images using the given {@link BitmapTransformation}s. + * + * @see #centerCrop() + * @see #fitCenter() + * @see #transform(Transformation[]) + * + * @param transformations The transformations to apply in order. + * @return This request builder. + */ + public BitmapRequestBuilder transform(BitmapTransformation... transformations) { + super.transform(transformations); + return this; + } + + /** + * Transform images using {@link com.bumptech.glide.load.resource.bitmap.CenterCrop}. + * + * @see #fitCenter() + * @see #transform(BitmapTransformation...) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + public BitmapRequestBuilder centerCrop() { + return transform(glide.getBitmapCenterCrop()); + } + + /** + * Transform images using {@link com.bumptech.glide.load.resource.bitmap.FitCenter}. + * + * @see #centerCrop() + * @see #transform(BitmapTransformation...) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + public BitmapRequestBuilder fitCenter() { + return transform(glide.getBitmapFitCenter()); + } + + /** + * {@inheritDoc} + * + * @see #fitCenter() + * @see #centerCrop() + */ + @Override + public BitmapRequestBuilder transform(Transformation... transformations) { + super.transform(transformations); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder transcoder( + ResourceTranscoder transcoder) { + super.transcoder(transcoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder dontAnimate() { + super.dontAnimate(); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder animate(int animationId) { + super.animate(animationId); + return this; + } + + /** + * {@inheritDoc} + */ + @Deprecated + @SuppressWarnings("deprecation") + @Override + public BitmapRequestBuilder animate(Animation animation) { + super.animate(animation); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder animate(ViewPropertyAnimation.Animator animator) { + super.animate(animator); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder placeholder(int resourceId) { + super.placeholder(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder placeholder(Drawable drawable) { + super.placeholder(drawable); + return this; + } + + @Override + public BitmapRequestBuilder fallback(Drawable drawable) { + super.fallback(drawable); + return this; + } + + @Override + public BitmapRequestBuilder fallback(int resourceId) { + super.fallback(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder error(int resourceId) { + super.error(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder error(Drawable drawable) { + super.error(drawable); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder listener( + RequestListener requestListener) { + super.listener(requestListener); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder skipMemoryCache(boolean skip) { + super.skipMemoryCache(skip); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder diskCacheStrategy(DiskCacheStrategy strategy) { + super.diskCacheStrategy(strategy); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder override(int width, int height) { + super.override(width, height); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder thumbnail( + GenericRequestBuilder thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder sourceEncoder(Encoder sourceEncoder) { + super.sourceEncoder(sourceEncoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public BitmapRequestBuilder dontTransform() { + super.dontTransform(); + return this; + } + + @Override + public BitmapRequestBuilder signature(Key signature) { + super.signature(signature); + return this; + } + + @Override + public BitmapRequestBuilder load(ModelType model) { + super.load(model); + return this; + } + + @Override + public BitmapRequestBuilder clone() { + return (BitmapRequestBuilder) super.clone(); + } + + /** + * {@inheritDoc} + * + *

+ * Note - If no transformation is set for this load, a default transformation will be applied based on the + * value returned from {@link ImageView#getScaleType()}. To avoid this default transformation, + * use {@link #dontTransform()}. + *

+ * + * @param view {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Target into(ImageView view) { + return super.into(view); + } + + @Override + void applyFitCenter() { + fitCenter(); + } + + @Override + void applyCenterCrop() { + centerCrop(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/BitmapTypeRequest.java b/core/src/main/java/com/example/bumptech/glide/BitmapTypeRequest.java new file mode 100755 index 0000000..dffaf8b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/BitmapTypeRequest.java @@ -0,0 +1,102 @@ +package com.example.bumptech.glide; + +import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.model.ImageVideoModelLoader; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.BitmapBytesTranscoder; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.provider.FixedLoadProvider; + +import java.io.InputStream; + +/** + * A class for creating a load request that either loads an {@link Bitmap} directly or that adds an + * {@link ResourceTranscoder} to transcode the {@link Bitmap} into another + * resource type. + * + * @param The type of model to load the {@link Bitmap} or transcoded class from. + */ +public class BitmapTypeRequest extends BitmapRequestBuilder { + private final ModelLoader streamModelLoader; + private final ModelLoader fileDescriptorModelLoader; + private final Glide glide; + private final RequestManager.OptionsApplier optionsApplier; + + private static FixedLoadProvider buildProvider(Glide glide, + ModelLoader streamModelLoader, + ModelLoader fileDescriptorModelLoader, + Class transcodedClass, ResourceTranscoder transcoder) { + if (streamModelLoader == null && fileDescriptorModelLoader == null) { + return null; + } + + if (transcoder == null) { + transcoder = glide.buildTranscoder(Bitmap.class, transcodedClass); + } + DataLoadProvider loadProvider = glide.buildDataProvider(ImageVideoWrapper.class, + Bitmap.class); + ImageVideoModelLoader modelLoader = new ImageVideoModelLoader(streamModelLoader, + fileDescriptorModelLoader); + + return new FixedLoadProvider(modelLoader, transcoder, loadProvider); + } + + BitmapTypeRequest(GenericRequestBuilder other, + ModelLoader streamModelLoader, + ModelLoader fileDescriptorModelLoader, + RequestManager.OptionsApplier optionsApplier) { + super(buildProvider(other.glide, streamModelLoader, fileDescriptorModelLoader, Bitmap.class, null), + Bitmap.class, other); + this.streamModelLoader = streamModelLoader; + this.fileDescriptorModelLoader = fileDescriptorModelLoader; + this.glide = other.glide; + this.optionsApplier = optionsApplier; + } + + /** + * Sets a transcoder to transcode the decoded and transformed {@link Bitmap} into another resource type. + * + * @param transcoder The transoder to use. + * @param transcodeClass The {@link Class} of the resource the {@link Bitmap} will be transcoded to. + * @param The type of the resource the {@link Bitmap} will be transcoded to. + * @return This request builder. + */ + public BitmapRequestBuilder transcode(ResourceTranscoder transcoder, + Class transcodeClass) { + return optionsApplier.apply(new BitmapRequestBuilder( + buildProvider(glide, streamModelLoader, fileDescriptorModelLoader, transcodeClass, transcoder), + transcodeClass, this)); + } + + /** + * Transcodes the decoded and transformed {@link Bitmap} to bytes by compressing it as a JPEG to a byte array. + * array. + * + * @see #toBytes(Bitmap.CompressFormat, int) + * + * @return This request builder. + */ + public BitmapRequestBuilder toBytes() { + return transcode(new BitmapBytesTranscoder(), byte[].class); + } + + /** + * Transcodes the decoded and transformed {@link Bitmap} to bytes by compressing it using the + * given format and quality to a byte array. + * + * @see Bitmap#compress(Bitmap.CompressFormat, int, java.io.OutputStream) + * @see #toBytes() + * + * @param compressFormat The {@link Bitmap.CompressFormat} to use to compress the {@link Bitmap}. + * @param quality The quality level from 0-100 to use to compress the {@link Bitmap}. + * @return This request builder. + */ + public BitmapRequestBuilder toBytes(Bitmap.CompressFormat compressFormat, int quality) { + return transcode(new BitmapBytesTranscoder(compressFormat, quality), byte[].class); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/BuildConfig.java b/core/src/main/java/com/example/bumptech/glide/BuildConfig.java new file mode 100755 index 0000000..47515e4 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/BuildConfig.java @@ -0,0 +1,13 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.example.bumptech.glide; + +public final class BuildConfig { + public static final boolean DEBUG = false; + public static final String APPLICATION_ID = "com.bumptech.glide"; + public static final String BUILD_TYPE = "release"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = 14; + public static final String VERSION_NAME = "3.7.0"; +} diff --git a/core/src/main/java/com/example/bumptech/glide/DownloadOptions.java b/core/src/main/java/com/example/bumptech/glide/DownloadOptions.java new file mode 100755 index 0000000..f3ec290 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/DownloadOptions.java @@ -0,0 +1,30 @@ +package com.example.bumptech.glide; + + +import com.example.bumptech.glide.request.FutureTarget; +import com.example.bumptech.glide.request.target.Target; + +import java.io.File; + +interface DownloadOptions { + + /** + * Loads the original unmodified data into the cache and calls the given Target with the cache File. + * + * @param target The Target that will receive the cache File when the load completes + * @param The type of Target. + * @return The given Target. + */ + > Y downloadOnly(Y target); + + + /** + * Loads the original unmodified data into the cache and returns a {@link java.util.concurrent.Future} that can be + * used to retrieve the cache File containing the data. + * + * @param width The width in pixels to use to fetch the data. + * @param height The height in pixels to use to fetch the data. + * @return A {@link java.util.concurrent.Future} that can be used to retrieve the cache File containing the data. + */ + FutureTarget downloadOnly(int width, int height); +} diff --git a/core/src/main/java/com/example/bumptech/glide/DrawableOptions.java b/core/src/main/java/com/example/bumptech/glide/DrawableOptions.java new file mode 100755 index 0000000..a5b3718 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/DrawableOptions.java @@ -0,0 +1,65 @@ +package com.example.bumptech.glide; + +import android.view.animation.Animation; + +interface DrawableOptions { + + /** + * Applies a cross fade transformation that fades from the placeholder to the loaded + * {@link android.graphics.drawable.Drawable}. If no placeholder is set, the Drawable will instead simply fade in. + * + * @see #crossFade(int) + * @see #crossFade(int, int) + * + * @return This request builder. + */ + GenericRequestBuilder crossFade(); + + /** + * Applies a cross fade transformation that fades from the placeholder to the loaded + * {@link android.graphics.drawable.Drawable}. If no placeholder is set the Drawable will instead simply fade in. + * + * @see #crossFade() + * @see #crossFade(int, int) + * + * @param duration The duration of the cross fade and initial fade in. + * @return This request builder. + */ + GenericRequestBuilder crossFade(int duration); + + + /** + * Applies a cross fade transformation that des from the placeholder to the loaded + * {@link android.graphics.drawable.Drawable}. If no placeholder is set, the Drawable will instead be animated in + * using the given {@link Animation}. + * + * @see #crossFade() + * @see #crossFade(int) + * @see #crossFade(int, int) + * + * @deprecated If this builder is used for multiple loads, using this method will result in multiple view's being + * asked to start an animation using a single {@link Animation} object which results in + * views animating repeatedly. Use {@link #crossFade(int, int)}} instead, or be sure to call this method once + * per call to {@link GenericRequestBuilder#load(Object)} to avoid re-using animation objects. + * Scheduled to be removed in Glide 4.0. + * @param animation The Animation to use if no placeholder is set. + * @param duration The duration of the cross fade animation. + * @return This request builder. + */ + @Deprecated + GenericRequestBuilder crossFade(Animation animation, int duration); + + /** + * Applies a cross fade transformation that des from the placeholder to the loaded + * {@link android.graphics.drawable.Drawable}. If no placeholder is set, the Drawable will instead be animated in + * using the {@link Animation} loaded from the given animation id. + * + * @see #crossFade() + * @see #crossFade(int) + * + * @param animationId The id of the Animation to use if no placeholder is set. + * @param duration The duration of the cross fade animation. + * @return This request builder. + */ + GenericRequestBuilder crossFade(int animationId, int duration); +} diff --git a/core/src/main/java/com/example/bumptech/glide/DrawableRequestBuilder.java b/core/src/main/java/com/example/bumptech/glide/DrawableRequestBuilder.java new file mode 100755 index 0000000..16982bd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/DrawableRequestBuilder.java @@ -0,0 +1,460 @@ +package com.example.bumptech.glide; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.view.animation.Animation; +import android.widget.ImageView; + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapper; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperTransformation; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.manager.Lifecycle; +import com.example.bumptech.glide.manager.RequestTracker; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.RequestListener; +import com.example.bumptech.glide.request.animation.DrawableCrossFadeFactory; +import com.example.bumptech.glide.request.animation.ViewPropertyAnimation; +import com.example.bumptech.glide.request.target.Target; + +import java.io.File; + +/** + * A class for creating a request to load a {@link GlideDrawable}. + * + *

+ * Warning - It is not safe to use this builder after calling into(), it may be pooled and + * reused. + *

+ * + * @param The type of model that will be loaded into the target. + */ +public class DrawableRequestBuilder + extends GenericRequestBuilder + implements BitmapOptions, DrawableOptions { + + DrawableRequestBuilder(Context context, Class modelClass, + LoadProvider loadProvider, Glide glide, + RequestTracker requestTracker, Lifecycle lifecycle) { + super(context, modelClass, loadProvider, GlideDrawable.class, glide, requestTracker, lifecycle); + // Default to animating. + crossFade(); + } + + /** + * Loads and displays the {@link GlideDrawable} retrieved by the given thumbnail request if it finishes before this + * request. Best used for loading thumbnail {@link GlideDrawable}s that are smaller and will be loaded more quickly + * than the fullsize {@link GlideDrawable}. There are no guarantees about the order in which the requests will + * actually finish. However, if the thumb request completes after the full request, the thumb {@link GlideDrawable} + * will never replace the full image. + * + * @see #thumbnail(float) + * + *

+ * Note - Any options on the main request will not be passed on to the thumbnail request. For example, if + * you want an animation to occur when either the full {@link GlideDrawable} loads or the thumbnail loads, + * you need to call {@link #animate(int)} on both the thumb and the full request. For a simpler thumbnail + * option where these options are applied to the humbnail as well, see {@link #thumbnail(float)}. + *

+ * + *

+ * Only the thumbnail call on the main request will be obeyed, recursive calls to this method are ignored. + *

+ * + * @param thumbnailRequest The request to use to load the thumbnail. + * @return This builder object. + */ + public DrawableRequestBuilder thumbnail( + DrawableRequestBuilder thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder thumbnail( + GenericRequestBuilder thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder thumbnail(float sizeMultiplier) { + super.thumbnail(sizeMultiplier); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder sizeMultiplier(float sizeMultiplier) { + super.sizeMultiplier(sizeMultiplier); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder decoder(ResourceDecoder decoder) { + super.decoder(decoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder cacheDecoder(ResourceDecoder cacheDecoder) { + super.cacheDecoder(cacheDecoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder encoder(ResourceEncoder encoder) { + super.encoder(encoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder priority(Priority priority) { + super.priority(priority); + return this; + } + + /** + * Transform {@link GlideDrawable}s using the given + * {@link BitmapTransformation}s. + * + *

+ * Note - Bitmap transformations will apply individually to each frame of animated GIF images and also to + * individual {@link Bitmap}s. + *

+ * + * @see #centerCrop() + * @see #fitCenter() + * @see #bitmapTransform(Transformation[]) + * @see #transform(Transformation[]) + * + * @param transformations The transformations to apply in order. + * @return This request builder. + */ + public DrawableRequestBuilder transform(BitmapTransformation... transformations) { + return bitmapTransform(transformations); + } + + /** + * Transform {@link GlideDrawable}s using {@link com.bumptech.glide.load.resource.bitmap.CenterCrop}. + * + * @see #fitCenter() + * @see #transform(BitmapTransformation...) + * @see #bitmapTransform(Transformation[]) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + @SuppressWarnings("unchecked") + public DrawableRequestBuilder centerCrop() { + return transform(glide.getDrawableCenterCrop()); + } + + /** + * Transform {@link GlideDrawable}s using {@link com.bumptech.glide.load.resource.bitmap.FitCenter}. + * + * @see #centerCrop() + * @see #transform(BitmapTransformation...) + * @see #bitmapTransform(Transformation[]) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + @SuppressWarnings("unchecked") + public DrawableRequestBuilder fitCenter() { + return transform(glide.getDrawableFitCenter()); + } + + /** + * Transform {@link GlideDrawable}s using the given {@link Bitmap} transformations. Replaces any + * previous transformations. + * + * @see #fitCenter() + * @see #centerCrop() + * @see #transform(BitmapTransformation...) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + public DrawableRequestBuilder bitmapTransform(Transformation... bitmapTransformations) { + GifBitmapWrapperTransformation[] transformations = + new GifBitmapWrapperTransformation[bitmapTransformations.length]; + for (int i = 0; i < bitmapTransformations.length; i++) { + transformations[i] = new GifBitmapWrapperTransformation(glide.getBitmapPool(), bitmapTransformations[i]); + } + return transform(transformations); + } + + + + /** + * {@inheritDoc} + * + * @see #bitmapTransform(Transformation[]) + * @see #centerCrop() + * @see #fitCenter() + */ + @Override + public DrawableRequestBuilder transform(Transformation... transformation) { + super.transform(transformation); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder transcoder( + ResourceTranscoder transcoder) { + super.transcoder(transcoder); + return this; + } + + /** + * {@inheritDoc} + */ + public final DrawableRequestBuilder crossFade() { + super.animate(new DrawableCrossFadeFactory()); + return this; + } + + /** + * {@inheritDoc} + */ + public DrawableRequestBuilder crossFade(int duration) { + super.animate(new DrawableCrossFadeFactory(duration)); + return this; + } + + /** + * {@inheritDoc} + */ + @Deprecated + public DrawableRequestBuilder crossFade(Animation animation, int duration) { + super.animate(new DrawableCrossFadeFactory(animation, duration)); + return this; + } + + /** + * {@inheritDoc} + */ + public DrawableRequestBuilder crossFade(int animationId, int duration) { + super.animate(new DrawableCrossFadeFactory(context, animationId, + duration)); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder dontAnimate() { + super.dontAnimate(); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder animate(ViewPropertyAnimation.Animator animator) { + super.animate(animator); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder animate(int animationId) { + super.animate(animationId); + return this; + } + + /** + * {@inheritDoc} + */ + @Deprecated + @SuppressWarnings("deprecation") + @Override + public DrawableRequestBuilder animate(Animation animation) { + super.animate(animation); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder placeholder(int resourceId) { + super.placeholder(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder placeholder(Drawable drawable) { + super.placeholder(drawable); + return this; + } + + @Override + public DrawableRequestBuilder fallback(Drawable drawable) { + super.fallback(drawable); + return this; + } + + @Override + public DrawableRequestBuilder fallback(int resourceId) { + super.fallback(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder error(int resourceId) { + super.error(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder error(Drawable drawable) { + super.error(drawable); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder listener( + RequestListener requestListener) { + super.listener(requestListener); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder diskCacheStrategy(DiskCacheStrategy strategy) { + super.diskCacheStrategy(strategy); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder skipMemoryCache(boolean skip) { + super.skipMemoryCache(skip); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder override(int width, int height) { + super.override(width, height); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder sourceEncoder(Encoder sourceEncoder) { + super.sourceEncoder(sourceEncoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public DrawableRequestBuilder dontTransform() { + super.dontTransform(); + return this; + } + + @Override + public DrawableRequestBuilder signature(Key signature) { + super.signature(signature); + return this; + } + + @Override + public DrawableRequestBuilder load(ModelType model) { + super.load(model); + return this; + } + + @Override + public DrawableRequestBuilder clone() { + return (DrawableRequestBuilder) super.clone(); + } + + /** + * {@inheritDoc} + * + *

+ * Note - If no transformation is set for this load, a default transformation will be applied based on the + * value returned from {@link ImageView#getScaleType()}. To avoid this default transformation, + * use {@link #dontTransform()}. + *

+ * + * @param view {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public Target into(ImageView view) { + return super.into(view); + } + + @Override + void applyFitCenter() { + fitCenter(); + } + + @Override + void applyCenterCrop() { + centerCrop(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/DrawableTypeRequest.java b/core/src/main/java/com/example/bumptech/glide/DrawableTypeRequest.java new file mode 100755 index 0000000..38e0196 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/DrawableTypeRequest.java @@ -0,0 +1,111 @@ +package com.example.bumptech.glide; + +import android.content.Context; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.model.ImageVideoModelLoader; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapper; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.manager.Lifecycle; +import com.example.bumptech.glide.manager.RequestTracker; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.provider.FixedLoadProvider; +import com.example.bumptech.glide.request.FutureTarget; +import com.example.bumptech.glide.request.target.Target; + +import java.io.File; +import java.io.InputStream; + +/** + * A class for creating a load request that loads either an animated GIF drawable or a Bitmap drawable directly, or + * adds an {@link ResourceTranscoder} to transcode the data into a + * resource type other than a {@link android.graphics.drawable.Drawable}. + * + * @param The type of model to use to load the {@link android.graphics.drawable.BitmapDrawable} or + * {@link com.bumptech.glide.load.resource.gif.GifDrawable}. + */ +public class DrawableTypeRequest extends DrawableRequestBuilder implements DownloadOptions { + private final ModelLoader streamModelLoader; + private final ModelLoader fileDescriptorModelLoader; + private final RequestManager.OptionsApplier optionsApplier; + + private static FixedLoadProvider buildProvider(Glide glide, + ModelLoader streamModelLoader, + ModelLoader fileDescriptorModelLoader, Class resourceClass, + Class transcodedClass, + ResourceTranscoder transcoder) { + if (streamModelLoader == null && fileDescriptorModelLoader == null) { + return null; + } + + if (transcoder == null) { + transcoder = glide.buildTranscoder(resourceClass, transcodedClass); + } + DataLoadProvider dataLoadProvider = glide.buildDataProvider(ImageVideoWrapper.class, + resourceClass); + ImageVideoModelLoader
modelLoader = new ImageVideoModelLoader(streamModelLoader, + fileDescriptorModelLoader); + return new FixedLoadProvider(modelLoader, transcoder, dataLoadProvider); + } + + DrawableTypeRequest(Class modelClass, ModelLoader streamModelLoader, + ModelLoader fileDescriptorModelLoader, Context context, Glide glide, + RequestTracker requestTracker, Lifecycle lifecycle, RequestManager.OptionsApplier optionsApplier) { + super(context, modelClass, + buildProvider(glide, streamModelLoader, fileDescriptorModelLoader, GifBitmapWrapper.class, + GlideDrawable.class, null), + glide, requestTracker, lifecycle); + this.streamModelLoader = streamModelLoader; + this.fileDescriptorModelLoader = fileDescriptorModelLoader; + this.optionsApplier = optionsApplier; + } + + /** + * Attempts to always load the resource as a {@link android.graphics.Bitmap}, even if it could actually be animated. + * + * @return A new request builder for loading a {@link android.graphics.Bitmap} + */ + public BitmapTypeRequest asBitmap() { + return optionsApplier.apply(new BitmapTypeRequest(this, streamModelLoader, + fileDescriptorModelLoader, optionsApplier)); + } + + /** + * Attempts to always load the resource as a {@link com.bumptech.glide.load.resource.gif.GifDrawable}. + *

+ * If the underlying data is not a GIF, this will fail. As a result, this should only be used if the model + * represents an animated GIF and the caller wants to interact with the GIfDrawable directly. Normally using + * just an {@link DrawableTypeRequest} is sufficient because it will determine whether or + * not the given data represents an animated GIF and return the appropriate animated or not animated + * {@link android.graphics.drawable.Drawable} automatically. + *

+ * + * @return A new request builder for loading a {@link com.bumptech.glide.load.resource.gif.GifDrawable}. + */ + public GifTypeRequest asGif() { + return optionsApplier.apply(new GifTypeRequest(this, streamModelLoader, optionsApplier)); + } + + /** + * {@inheritDoc} + */ + public > Y downloadOnly(Y target) { + return getDownloadOnlyRequest().downloadOnly(target); + } + + /** + * {@inheritDoc} + */ + public FutureTarget downloadOnly(int width, int height) { + return getDownloadOnlyRequest().downloadOnly(width, height); + } + + private GenericTranscodeRequest getDownloadOnlyRequest() { + return optionsApplier.apply(new GenericTranscodeRequest(File.class, this, + streamModelLoader, InputStream.class, File.class, optionsApplier)); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/GenericRequestBuilder.java b/core/src/main/java/com/example/bumptech/glide/GenericRequestBuilder.java new file mode 100755 index 0000000..1357014 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/GenericRequestBuilder.java @@ -0,0 +1,861 @@ +package com.example.bumptech.glide; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.animation.Animation; +import android.widget.ImageView; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.MultiTransformation; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.resource.UnitTransformation; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.manager.Lifecycle; +import com.example.bumptech.glide.manager.RequestTracker; +import com.example.bumptech.glide.provider.ChildLoadProvider; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.FutureTarget; +import com.example.bumptech.glide.request.GenericRequest; +import com.example.bumptech.glide.request.Request; +import com.example.bumptech.glide.request.RequestCoordinator; +import com.example.bumptech.glide.request.RequestFutureTarget; +import com.example.bumptech.glide.request.RequestListener; +import com.example.bumptech.glide.request.ThumbnailRequestCoordinator; +import com.example.bumptech.glide.request.animation.GlideAnimationFactory; +import com.example.bumptech.glide.request.animation.NoAnimation; +import com.example.bumptech.glide.request.animation.ViewAnimationFactory; +import com.example.bumptech.glide.request.animation.ViewPropertyAnimation; +import com.example.bumptech.glide.request.animation.ViewPropertyAnimationFactory; +import com.example.bumptech.glide.request.target.PreloadTarget; +import com.example.bumptech.glide.request.target.Target; +import com.example.bumptech.glide.signature.EmptySignature; +import com.example.bumptech.glide.util.Util; + +import java.io.File; + +/** + * A generic class that can handle setting options and staring loads for generic resource types. + * + * @param The type of model representing the resource. + * @param The data type that the resource {@link com.bumptech.glide.load.model.ModelLoader} will provide that + * can be decoded by the {@link ResourceDecoder}. + * @param The type of the resource that will be loaded. + * @param The type of resource the decoded resource will be transcoded to. + */ +public class GenericRequestBuilder implements Cloneable { + protected final Class modelClass; + protected final Context context; + protected final Glide glide; + protected final Class transcodeClass; + protected final RequestTracker requestTracker; + protected final Lifecycle lifecycle; + private ChildLoadProvider loadProvider; + + private ModelType model; + private Key signature = EmptySignature.obtain(); + // model may occasionally be null, so to enforce that load() was called, set a boolean rather than relying on model + // not to be null. + private boolean isModelSet; + private int placeholderId; + private int errorId; + private RequestListener requestListener; + private Float thumbSizeMultiplier; + private GenericRequestBuilder thumbnailRequestBuilder; + private Float sizeMultiplier = 1f; + private Drawable placeholderDrawable; + private Drawable errorPlaceholder; + private Priority priority = null; + private boolean isCacheable = true; + private GlideAnimationFactory animationFactory = NoAnimation.getFactory(); + private int overrideHeight = -1; + private int overrideWidth = -1; + private DiskCacheStrategy diskCacheStrategy = DiskCacheStrategy.RESULT; + private Transformation transformation = UnitTransformation.get(); + private boolean isTransformationSet; + private boolean isThumbnailBuilt; + private Drawable fallbackDrawable; + private int fallbackResource; + + GenericRequestBuilder(LoadProvider loadProvider, + Class transcodeClass, GenericRequestBuilder other) { + this(other.context, other.modelClass, loadProvider, transcodeClass, other.glide, other.requestTracker, + other.lifecycle); + this.model = other.model; + this.isModelSet = other.isModelSet; + this.signature = other.signature; + this.diskCacheStrategy = other.diskCacheStrategy; + this.isCacheable = other.isCacheable; + } + + GenericRequestBuilder(Context context, Class modelClass, + LoadProvider loadProvider, + Class transcodeClass, Glide glide, RequestTracker requestTracker, Lifecycle lifecycle) { + this.context = context; + this.modelClass = modelClass; + this.transcodeClass = transcodeClass; + this.glide = glide; + this.requestTracker = requestTracker; + this.lifecycle = lifecycle; + this.loadProvider = loadProvider != null + ? new ChildLoadProvider(loadProvider) : null; + + if (context == null) { + throw new NullPointerException("Context can't be null"); + } + if (modelClass != null && loadProvider == null) { + throw new NullPointerException("LoadProvider must not be null"); + } + } + + /** + * Loads and displays the resource retrieved by the given thumbnail request if it finishes before this request. + * Best used for loading thumbnail resources that are smaller and will be loaded more quickly than the full size + * resource. There are no guarantees about the order in which the requests will actually finish. However, if the + * thumb request completes after the full request, the thumb resource will never replace the full resource. + * + * @see #thumbnail(float) + * + *

+ * Recursive calls to thumbnail are supported. + *

+ * + * @param thumbnailRequest The request to use to load the thumbnail. + * @return This request builder. + */ + public GenericRequestBuilder thumbnail( + GenericRequestBuilder thumbnailRequest) { + if (this.equals(thumbnailRequest)) { + throw new IllegalArgumentException("You cannot set a request as a thumbnail for itself. Consider using " + + "clone() on the request you are passing to thumbnail()"); + } + this.thumbnailRequestBuilder = thumbnailRequest; + + return this; + } + + /** + * Loads a resource in an identical manner to this request except with the dimensions of the target multiplied + * by the given size multiplier. If the thumbnail load completes before the fullsize load, the thumbnail will + * be shown. If the thumbnail load completes afer the fullsize load, the thumbnail will not be shown. + * + *

+ * Note - The thumbnail resource will be smaller than the size requested so the target (or {@link ImageView}) + * must be able to scale the thumbnail appropriately. See {@link ImageView.ScaleType}. + *

+ * + *

+ * Almost all options will be copied from the original load, including the + * {@link com.bumptech.glide.load.model.ModelLoader}, {@link ResourceDecoder}, and + * {@link Transformation}s. However, {@link #placeholder(int)} and {@link #error(int)}, + * and {@link #listener(RequestListener)} will only be used on the fullsize load and will not be copied for + * the thumbnail load. + *

+ * + *

+ * Recursive calls to thumbnail are supported. + *

+ * + * @param sizeMultiplier The multiplier to apply to the {@link Target}'s dimensions when loading the thumbnail. + * @return This request builder. + */ + public GenericRequestBuilder thumbnail( + float sizeMultiplier) { + if (sizeMultiplier < 0f || sizeMultiplier > 1f) { + throw new IllegalArgumentException("sizeMultiplier must be between 0 and 1"); + } + this.thumbSizeMultiplier = sizeMultiplier; + + return this; + } + + /** + * Applies a multiplier to the {@link Target}'s size before loading the resource. Useful for loading thumbnails + * or trying to avoid loading huge resources (particularly {@link android.graphics.Bitmap}s on devices with overly + * dense screens. + * + * @param sizeMultiplier The multiplier to apply to the {@link Target}'s dimensions when loading the resource. + * @return This request builder. + */ + public GenericRequestBuilder sizeMultiplier( + float sizeMultiplier) { + if (sizeMultiplier < 0f || sizeMultiplier > 1f) { + throw new IllegalArgumentException("sizeMultiplier must be between 0 and 1"); + } + this.sizeMultiplier = sizeMultiplier; + + return this; + } + + /** + * Sets the {@link ResourceDecoder} to use to load the resource from the original data. + * By default, this decoder will only be used if the final transformed resource is not in the disk cache. + * + * @see #cacheDecoder(ResourceDecoder) + * @see DiskCacheStrategy + * + * @param decoder The {@link ResourceDecoder} to use to decode the resource. + * @return This request builder. + */ + public GenericRequestBuilder decoder( + ResourceDecoder decoder) { + // loadProvider will be null if model is null, in which case we're not going to load anything so it's ok to + // ignore the decoder. + if (loadProvider != null) { + loadProvider.setSourceDecoder(decoder); + } + + return this; + } + + /** + * Sets the {@link ResourceDecoder} to use to load the resource from the disk cache. By + * default, this decoder will only be used if the final transformed resource is already in the disk cache. + * + * @see #decoder(ResourceDecoder) + * @see DiskCacheStrategy + * + * @param cacheDecoder The decoder to use. + * @return This request builder. + */ + public GenericRequestBuilder cacheDecoder( + ResourceDecoder cacheDecoder) { + // loadProvider will be null if model is null, in which case we're not going to load anything so it's ok to + // ignore the decoder. + if (loadProvider != null) { + loadProvider.setCacheDecoder(cacheDecoder); + } + + return this; + } + + /** + * Sets the source encoder to use to encode the data retrieved by this request directly into cache. The returned + * resource will then be decoded from the cached data. + * + * @see DiskCacheStrategy + * + * @param sourceEncoder The encoder to use. + * @return This request builder. + */ + public GenericRequestBuilder sourceEncoder( + Encoder sourceEncoder) { + if (loadProvider != null) { + loadProvider.setSourceEncoder(sourceEncoder); + } + + return this; + } + + /** + * Sets the {@link DiskCacheStrategy} to use for this load. Defaults to + * {@link DiskCacheStrategy#RESULT}. + * + *

+ * For most applications {@link DiskCacheStrategy#RESULT} is ideal. + * Applications that use the same resource multiple times in multiple sizes and are willing to trade off some + * speed and disk space in return for lower bandwidth usage may want to consider using + * {@link DiskCacheStrategy#SOURCE} or + * {@link DiskCacheStrategy#RESULT}. Any download only operations should + * typically use {@link DiskCacheStrategy#SOURCE}. + *

+ * + * @param strategy The strategy to use. + * @return This request builder. + */ + public GenericRequestBuilder diskCacheStrategy( + DiskCacheStrategy strategy) { + this.diskCacheStrategy = strategy; + + return this; + } + + /** + * Sets the {@link Encoder} to use to encode the original data directly to cache. Will only + * be used if the original data is not already in cache and if the + * {@link DiskCacheStrategy} is set to + * {@link DiskCacheStrategy#SOURCE} or + * {@link DiskCacheStrategy#ALL}. + * + * @see #sourceEncoder(Encoder) + * @see DiskCacheStrategy + * + * @param encoder The encoder to use. + * @return This request builder. + */ + public GenericRequestBuilder encoder( + ResourceEncoder encoder) { + // loadProvider will be null if model is null, in which case we're not going to load anything so it's ok to + // ignore the encoder. + if (loadProvider != null) { + loadProvider.setEncoder(encoder); + } + + return this; + } + + /** + * Sets the priority for this load. + * + * @param priority A priority. + * @return This request builder. + */ + public GenericRequestBuilder priority( + Priority priority) { + this.priority = priority; + + return this; + } + + /** + * Transform resources with the given {@link Transformation}s. Replaces any existing transformation or + * transformations. + * + * @param transformations the transformations to apply in order. + * @return This request builder. + */ + public GenericRequestBuilder transform( + Transformation... transformations) { + isTransformationSet = true; + if (transformations.length == 1) { + transformation = transformations[0]; + } else { + transformation = new MultiTransformation(transformations); + } + + return this; + } + + /** + * Removes the current {@link Transformation}. + * + * @return This request builder. + */ + @SuppressWarnings("unchecked") + public GenericRequestBuilder dontTransform() { + Transformation transformation = UnitTransformation.get(); + return transform(transformation); + } + + /** + * Sets the {@link ResourceTranscoder} to use for this load. + * + * + * @param transcoder The transcoder to use. + * @return This request builder. + */ + public GenericRequestBuilder transcoder( + ResourceTranscoder transcoder) { + if (loadProvider != null) { + loadProvider.setTranscoder(transcoder); + } + + return this; + } + + /** + * Removes any existing animation set on the builder. Will be overridden by subsequent calls that set an animation. + * @return This request builder. + */ + public GenericRequestBuilder dontAnimate() { + GlideAnimationFactory animation = NoAnimation.getFactory(); + return animate(animation); + } + + /** + * Sets an animation to run on the wrapped target when an resource load finishes. Will only be run if the resource + * was loaded asynchronously (ie was not in the memory cache) + * + * @param animationId The resource id of the animation to run + * @return This request builder. + */ + public GenericRequestBuilder animate(int animationId) { + return animate(new ViewAnimationFactory(context, animationId)); + } + + /** + * Sets an animation to run on the wrapped target when a resource load finishes. Will only be run if the resource + * was loaded asynchronously (ie was not in the memory cache) + * + * @see #animate(int) + * @see #animate(ViewPropertyAnimation.Animator) + * + * @deprecated If this builder is used for multiple loads, using this method will result in multiple view's being + * asked to start an animation using a single {@link Animation} object which results in + * views animating repeatedly. Use {@link #animate(int)} or + * {@link #animate(ViewPropertyAnimation.Animator)}. Scheduled to be removed in + * Glide 4.0. + * @param animation The animation to run + * @return This request builder. + */ + @Deprecated + public GenericRequestBuilder animate(Animation animation) { + return animate(new ViewAnimationFactory(animation)); + } + + /** + * Sets an animator to run a {@link android.view.ViewPropertyAnimator} on a view that the target may be wrapping + * when a resource load finishes. Will only be run if the load was loaded asynchronously (ie was not in the + * memory cache). + * + * @param animator The {@link ViewPropertyAnimation.Animator} to run. + * @return This request builder. + */ + public GenericRequestBuilder animate( + ViewPropertyAnimation.Animator animator) { + return animate(new ViewPropertyAnimationFactory(animator)); + } + + GenericRequestBuilder animate( + GlideAnimationFactory animationFactory) { + if (animationFactory == null) { + throw new NullPointerException("Animation factory must not be null!"); + } + this.animationFactory = animationFactory; + + return this; + } + + /** + * Sets an Android resource id for a {@link Drawable} resourceto display while a resource + * is loading. + * + * @param resourceId The id of the resource to use as a placeholder + * @return This request builder. + */ + public GenericRequestBuilder placeholder( + int resourceId) { + this.placeholderId = resourceId; + + return this; + } + + /** + * Sets an {@link Drawable} to display while a resource is loading. + * + * @param drawable The drawable to display as a placeholder. + * @return This request builder. + */ + public GenericRequestBuilder placeholder( + Drawable drawable) { + this.placeholderDrawable = drawable; + + return this; + } + + /** + * Sets an {@link Drawable} to display if the model provided to + * {@link #load(Object)} is {@code null}. + * + *

+ * If a fallback is not set, null models will cause the error drawable to be displayed. If + * the error drawable is not set, the placeholder will be displayed. + *

+ * + * @see #placeholder(Drawable) + * @see #placeholder(int) + * + * @param drawable The drawable to display as a placeholder. + * @return This request builder. + */ + public GenericRequestBuilder fallback( + Drawable drawable) { + this.fallbackDrawable = drawable; + + return this; + } + + /** + * Sets a resource to display if the model provided to {@link #load(Object)} is {@code null}. + * + *

+ * If a fallback is not set, null models will cause the error drawable to be displayed. If + * the error drawable is not set, the placeholder will be displayed. + *

+ * + * @see #placeholder(Drawable) + * @see #placeholder(int) + * + * @param resourceId The id of the resource to use as a fallback. + * @return This request builder. + */ + public GenericRequestBuilder fallback( + int resourceId) { + this.fallbackResource = resourceId; + + return this; + } + + /** + * Sets a resource to display if a load fails. + * + * @param resourceId The id of the resource to use as a placeholder. + * @return This request builder. + */ + public GenericRequestBuilder error( + int resourceId) { + this.errorId = resourceId; + + return this; + } + + /** + * Sets a {@link Drawable} to display if a load fails. + * + * @param drawable The drawable to display. + * @return This request builder. + */ + public GenericRequestBuilder error( + Drawable drawable) { + this.errorPlaceholder = drawable; + + return this; + } + + /** + * Sets a RequestBuilder listener to monitor the resource load. It's best to create a single instance of an + * exception handler per type of request (usually activity/fragment) rather than pass one in per request to + * avoid some redundant object allocation. + * + * @param requestListener The request listener to use. + * @return This request builder. + */ + public GenericRequestBuilder listener( + RequestListener requestListener) { + this.requestListener = requestListener; + + return this; + } + + /** + * Allows the loaded resource to skip the memory cache. + * + *

+ * Note - this is not a guarantee. If a request is already pending for this resource and that request is not + * also skipping the memory cache, the resource will be cached in memory. + *

+ * + * @param skip True to allow the resource to skip the memory cache. + * @return This request builder. + */ + public GenericRequestBuilder skipMemoryCache(boolean skip) { + this.isCacheable = !skip; + + return this; + } + + /** + * Overrides the {@link Target}'s width and height with the given values. This is useful almost exclusively for + * thumbnails, and should only be used when you both need a very specific sized image and when it is impossible or + * impractical to return that size from {@link Target#getSize(com.bumptech.glide.request.target.SizeReadyCallback)}. + * + * @param width The width in pixels to use to load the resource. + * @param height The height in pixels to use to load the resource. + * @return This request builder. + */ + public GenericRequestBuilder override(int width, int height) { + if (!Util.isValidDimensions(width, height)) { + throw new IllegalArgumentException("Width and height must be Target#SIZE_ORIGINAL or > 0"); + } + this.overrideWidth = width; + this.overrideHeight = height; + + return this; + } + + /** + * Sets some additional data to be mixed in to the memory and disk cache keys allowing the caller more control over + * when cached data is invalidated. + * + *

+ * Note - The signature does not replace the cache key, it is purely additive. + *

+ * + * @see com.bumptech.glide.signature.StringSignature + * + * @param signature A unique non-null {@link Key} representing the current state of the + * model that will be mixed in to the cache key. + * @return This request builder. + */ + public GenericRequestBuilder signature(Key signature) { + if (signature == null) { + throw new NullPointerException("Signature must not be null"); + } + this.signature = signature; + return this; + } + + /** + * Sets the specific model to load data for. + * + *

+ * This method must be called at least once before {@link #into(Target)} is + * called. + *

+ * + * @param model The model to load data for, or null. + * @return This request builder. + */ + public GenericRequestBuilder load(ModelType model) { + this.model = model; + isModelSet = true; + return this; + } + + /** + * Returns a copy of this request builder with all of the options set so far on this builder. + * + *

+ * This method returns a "deep" copy in that all non-immutable arguments are copied such that changes to one + * builder will not affect the other builder. However, in addition to immutable arguments, the current model + * is not copied copied so changes to the model will affect both builders. + *

+ */ + @SuppressWarnings("unchecked") + @Override + public GenericRequestBuilder clone() { + try { + GenericRequestBuilder clone = + (GenericRequestBuilder) super.clone(); + clone.loadProvider = loadProvider != null ? loadProvider.clone() : null; + return clone; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + /** + * Set the target the resource will be loaded into. + * + * @see Glide#clear(Target) + * + * @param target The target to load the resource into. + * @return The given target. + */ + public > Y into(Y target) { + Util.assertMainThread(); + if (target == null) { + throw new IllegalArgumentException("You must pass in a non null Target"); + } + if (!isModelSet) { + throw new IllegalArgumentException("You must first set a model (try #load())"); + } + + Request previous = target.getRequest(); + + if (previous != null) { + previous.clear(); + requestTracker.removeRequest(previous); + previous.recycle(); + } + + Request request = buildRequest(target); + target.setRequest(request); + lifecycle.addListener(target); + requestTracker.runRequest(request); + + return target; + } + + /** + * Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into the view, and frees + * any resources Glide may have previously loaded into the view so they may be reused. + * + * @see Glide#clear(android.view.View) + * + * @param view The view to cancel previous loads for and load the new resource into. + * @return The {@link Target} used to wrap the given {@link ImageView}. + */ + public Target into(ImageView view) { + Util.assertMainThread(); + if (view == null) { + throw new IllegalArgumentException("You must pass in a non null View"); + } + + if (!isTransformationSet && view.getScaleType() != null) { + switch (view.getScaleType()) { + case CENTER_CROP: + applyCenterCrop(); + break; + case FIT_CENTER: + case FIT_START: + case FIT_END: + applyFitCenter(); + break; + //$CASES-OMITTED$ + default: + // Do nothing. + } + } + + return into(glide.buildImageViewTarget(view, transcodeClass)); + } + + /** + * Returns a future that can be used to do a blocking get on a background thread. + * + * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be overridden by + * {@link #override * (int, int)} if previously called. + * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be overridden by + * {@link #override * (int, int)}} if previously called). + * + * @see Glide#clear(FutureTarget) + * + * @return An {@link FutureTarget} that can be used to obtain the + * resource in a blocking manner. + */ + public FutureTarget into(int width, int height) { + final RequestFutureTarget target = + new RequestFutureTarget(glide.getMainHandler(), width, height); + + // TODO: Currently all loads must be started on the main thread... + glide.getMainHandler().post(new Runnable() { + @Override + public void run() { + if (!target.isCancelled()) { + into(target); + } + } + }); + + return target; + } + + /** + * Preloads the resource into the cache using the given width and height. + * + *

+ * Pre-loading is useful for making sure that resources you are going to to want in the near future are + * available quickly. + *

+ * + * + * @see ListPreloader + * + * @param width The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be overridden by + * {@link #override * (int, int)} if previously called. + * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be overridden by + * {@link #override * (int, int)}} if previously called). + * @return A {@link Target} that can be used to cancel the load via + * {@link Glide#clear(Target)}. + */ + public Target preload(int width, int height) { + final PreloadTarget target = PreloadTarget.obtain(width, height); + return into(target); + } + + /** + * Preloads the resource into the cache using {@link Target#SIZE_ORIGINAL} as the target width and height. + * Equivalent to calling {@link #preload(int, int)} with {@link Target#SIZE_ORIGINAL} as the width and height. + * + * @see #preload(int, int) + * + * @return A {@link Target} that can be used to cancel the load via + * {@link Glide#clear(Target)}. + */ + public Target preload() { + return preload(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); + } + + void applyCenterCrop() { + // To be implemented by subclasses when possible. + } + + void applyFitCenter() { + // To be implemented by subclasses when possible. + } + + private Priority getThumbnailPriority() { + final Priority result; + if (priority == Priority.LOW) { + result = Priority.NORMAL; + } else if (priority == Priority.NORMAL) { + result = Priority.HIGH; + } else { + result = Priority.IMMEDIATE; + } + return result; + } + + private Request buildRequest(Target target) { + if (priority == null) { + priority = Priority.NORMAL; + } + return buildRequestRecursive(target, null); + } + + private Request buildRequestRecursive(Target target, ThumbnailRequestCoordinator parentCoordinator) { + if (thumbnailRequestBuilder != null) { + if (isThumbnailBuilt) { + throw new IllegalStateException("You cannot use a request as both the main request and a thumbnail, " + + "consider using clone() on the request(s) passed to thumbnail()"); + } + // Recursive case: contains a potentially recursive thumbnail request builder. + if (thumbnailRequestBuilder.animationFactory.equals(NoAnimation.getFactory())) { + thumbnailRequestBuilder.animationFactory = animationFactory; + } + + if (thumbnailRequestBuilder.priority == null) { + thumbnailRequestBuilder.priority = getThumbnailPriority(); + } + + if (Util.isValidDimensions(overrideWidth, overrideHeight) + && !Util.isValidDimensions(thumbnailRequestBuilder.overrideWidth, + thumbnailRequestBuilder.overrideHeight)) { + thumbnailRequestBuilder.override(overrideWidth, overrideHeight); + } + + ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator); + Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator); + // Guard against infinite recursion. + isThumbnailBuilt = true; + // Recursively generate thumbnail requests. + Request thumbRequest = thumbnailRequestBuilder.buildRequestRecursive(target, coordinator); + isThumbnailBuilt = false; + coordinator.setRequests(fullRequest, thumbRequest); + return coordinator; + } else if (thumbSizeMultiplier != null) { + // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse. + ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator); + Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator); + Request thumbnailRequest = obtainRequest(target, thumbSizeMultiplier, getThumbnailPriority(), coordinator); + coordinator.setRequests(fullRequest, thumbnailRequest); + return coordinator; + } else { + // Base case: no thumbnail. + return obtainRequest(target, sizeMultiplier, priority, parentCoordinator); + } + } + + private Request obtainRequest(Target target, float sizeMultiplier, Priority priority, + RequestCoordinator requestCoordinator) { + return GenericRequest.obtain( + loadProvider, + model, + signature, + context, + priority, + target, + sizeMultiplier, + placeholderDrawable, + placeholderId, + errorPlaceholder, + errorId, + fallbackDrawable, + fallbackResource, + requestListener, + requestCoordinator, + glide.getEngine(), + transformation, + transcodeClass, + isCacheable, + animationFactory, + overrideWidth, + overrideHeight, + diskCacheStrategy); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/GenericTranscodeRequest.java b/core/src/main/java/com/example/bumptech/glide/GenericTranscodeRequest.java new file mode 100755 index 0000000..0f22c14 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/GenericTranscodeRequest.java @@ -0,0 +1,111 @@ +package com.example.bumptech.glide; + +import android.content.Context; + + +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.load.resource.transcode.UnitTranscoder; +import com.example.bumptech.glide.manager.Lifecycle; +import com.example.bumptech.glide.manager.RequestTracker; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.provider.FixedLoadProvider; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.FutureTarget; +import com.example.bumptech.glide.request.target.Target; + +import java.io.File; + +/** + * A class for handling requests to load a generic resource type or transcode the generic resource type into another + * generic resource type. + * + *

+ * Warning - It is not safe to use this builder after calling into(), it may be pooled and + * reused. + *

+ * + * @param The type of the model used to retrieve data. + * @param The type of data retrieved. + * @param The type of resource to be decoded from the the data. + */ +public class GenericTranscodeRequest + extends GenericRequestBuilder implements DownloadOptions { + private final ModelLoader modelLoader; + private final Class dataClass; + private final Class resourceClass; + private final RequestManager.OptionsApplier optionsApplier; + + private static LoadProvider build(Glide glide, ModelLoader modelLoader, + Class dataClass, Class resourceClass, ResourceTranscoder transcoder) { + DataLoadProvider dataLoadProvider = glide.buildDataProvider(dataClass, resourceClass); + return new FixedLoadProvider(modelLoader, transcoder, dataLoadProvider); + } + + GenericTranscodeRequest( + Class transcodeClass, GenericRequestBuilder other, + ModelLoader modelLoader, Class dataClass, Class resourceClass, + RequestManager.OptionsApplier optionsApplier) { + super(build(other.glide, modelLoader, dataClass, resourceClass, UnitTranscoder.get()), + transcodeClass, other); + this.modelLoader = modelLoader; + this.dataClass = dataClass; + this.resourceClass = resourceClass; + this.optionsApplier = optionsApplier; + } + + GenericTranscodeRequest(Context context, Glide glide, Class modelClass, + ModelLoader modelLoader, Class dataClass, Class resourceClass, + RequestTracker requestTracker, Lifecycle lifecycle, RequestManager.OptionsApplier optionsApplier) { + super(context, modelClass, build(glide, modelLoader, dataClass, resourceClass, + UnitTranscoder.get()), resourceClass, glide, requestTracker, lifecycle); + this.modelLoader = modelLoader; + this.dataClass = dataClass; + this.resourceClass = resourceClass; + this.optionsApplier = optionsApplier; + } + + /** + * Adds a transcoder to this request to transcode from the resource type to the given transcode type. + * + * @param transcoder The transcoder to use. + * @param transcodeClass The class of the resource type that will be transcoded to. + * @param The type of the resource that will be transcoded to. + * @return A new request builder to set options for the transcoded load. + */ + public GenericRequestBuilder transcode( + ResourceTranscoder transcoder, Class transcodeClass) { + LoadProvider loadProvider = build(glide, modelLoader, + dataClass, resourceClass, transcoder); + + return optionsApplier.apply(new GenericRequestBuilder( + loadProvider, transcodeClass, this)); + } + + /** + * {@inheritDoc} + */ + public > Y downloadOnly(Y target) { + return getDownloadOnlyRequest().into(target); + } + + /** + * {@inheritDoc} + */ + public FutureTarget downloadOnly(int width, int height) { + return getDownloadOnlyRequest().into(width, height); + } + + private GenericRequestBuilder getDownloadOnlyRequest() { + ResourceTranscoder transcoder = UnitTranscoder.get(); + DataLoadProvider dataLoadProvider = glide.buildDataProvider(dataClass, File.class); + FixedLoadProvider fixedLoadProvider = + new FixedLoadProvider(modelLoader, transcoder, dataLoadProvider); + return optionsApplier.apply(new GenericRequestBuilder(fixedLoadProvider, + File.class, this)) + .priority(Priority.LOW) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .skipMemoryCache(true); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/GifRequestBuilder.java b/core/src/main/java/com/example/bumptech/glide/GifRequestBuilder.java new file mode 100755 index 0000000..489c9f1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/GifRequestBuilder.java @@ -0,0 +1,434 @@ +package com.example.bumptech.glide; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.view.animation.Animation; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.load.resource.gif.GifDrawableTransformation; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.RequestListener; +import com.example.bumptech.glide.request.animation.DrawableCrossFadeFactory; +import com.example.bumptech.glide.request.animation.ViewPropertyAnimation; + +import java.io.File; +import java.io.InputStream; + +/** + * A class for creating a request to load an animated gif. + * + *

+ * Warning - It is not safe to use this builder after calling into(), it may be pooled and + * reused. + *

+ * + * @param The type of model that will be loaded into the target. + */ +public class GifRequestBuilder + extends GenericRequestBuilder + implements BitmapOptions, DrawableOptions { + + GifRequestBuilder(LoadProvider loadProvider, + Class transcodeClass, GenericRequestBuilder other) { + super(loadProvider, transcodeClass, other); + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder thumbnail(GenericRequestBuilder thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * Loads and displays the GIF retrieved by the given thumbnail request if it finishes before this + * request. Best used for loading thumbnail GIFs that are smaller and will be loaded more quickly + * than the fullsize GIF. There are no guarantees about the order in which the requests will actually + * finish. However, if the thumb request completes after the full request, the thumb GIF will never + * replace the full image. + * + * @see #thumbnail(float) + * + *

+ * Note - Any options on the main request will not be passed on to the thumbnail request. For example, if + * you want an animation to occur when either the full GIF loads or the thumbnail loads, + * you need to call {@link #animate(int)} on both the thumb and the full request. For a simpler thumbnail + * option where these options are applied to the humbnail as well, see {@link #thumbnail(float)}. + *

+ * + *

+ * Only the thumbnail call on the main request will be obeyed, recursive calls to this method are ignored. + *

+ * + * @param thumbnailRequest The request to use to load the thumbnail. + * @return This builder object. + */ + public GifRequestBuilder thumbnail(GifRequestBuilder thumbnailRequest) { + super.thumbnail(thumbnailRequest); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder thumbnail(float sizeMultiplier) { + super.thumbnail(sizeMultiplier); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder sizeMultiplier(float sizeMultiplier) { + super.sizeMultiplier(sizeMultiplier); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder decoder( + ResourceDecoder decoder) { + super.decoder(decoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder cacheDecoder( + ResourceDecoder cacheDecoder) { + super.cacheDecoder(cacheDecoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder encoder( + ResourceEncoder encoder) { + super.encoder(encoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder priority(Priority priority) { + super.priority(priority); + return this; + } + + /** + * Transforms each frame of the GIF using {@link com.bumptech.glide.load.resource.bitmap.CenterCrop}. + * + * @see #fitCenter() + * @see #transformFrame(BitmapTransformation...) + * @see #transformFrame(Transformation[]) + * @see #transform(Transformation[]) + * + * @return This request builder. + */ + public GifRequestBuilder centerCrop() { + return transformFrame(glide.getBitmapCenterCrop()); + } + + /** + * Transforms each frame of the GIF using {@link com.bumptech.glide.load.resource.bitmap.FitCenter}. + * + * @see #centerCrop() + * @see #transformFrame(BitmapTransformation...) + * @see #transformFrame(Transformation[]) + * @see #transform(Transformation[]) + * + * @return This request builder.. + */ + public GifRequestBuilder fitCenter() { + return transformFrame(glide.getBitmapFitCenter()); + } + + /** + * Transforms each frame of the GIF using the given transformations. + * + * @see #centerCrop() + * @see #fitCenter() + * @see #transformFrame(Transformation[]) + * @see #transform(Transformation[]) + * + * @param bitmapTransformations The transformations to apply in order to each frame. + * @return This request builder. + */ + public GifRequestBuilder transformFrame(BitmapTransformation... bitmapTransformations) { + return transform(toGifTransformations(bitmapTransformations)); + } + + /** + * Transforms each frame of the GIF using the given transformations. + * + * @see #fitCenter() + * @see #centerCrop() + * @see #transformFrame(BitmapTransformation...) + * @see #transform(Transformation[]) + * + * @param bitmapTransformations The transformations to apply in order to each frame. + * @return This request builder. + */ + public GifRequestBuilder transformFrame(Transformation... bitmapTransformations) { + return transform(toGifTransformations(bitmapTransformations)); + } + + private GifDrawableTransformation[] toGifTransformations(Transformation[] bitmapTransformations) { + GifDrawableTransformation[] transformations = new GifDrawableTransformation[bitmapTransformations.length]; + for (int i = 0; i < bitmapTransformations.length; i++) { + transformations[i] = new GifDrawableTransformation(bitmapTransformations[i], glide.getBitmapPool()); + } + return transformations; + } + + /** + * {@inheritDoc} + * + * @see #fitCenter() + * @see #centerCrop() + * @see #transformFrame(BitmapTransformation...) + * @see #transformFrame(Transformation[]) + * + */ + @Override + public GifRequestBuilder transform(Transformation... transformations) { + super.transform(transformations); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder transcoder(ResourceTranscoder transcoder) { + super.transcoder(transcoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder crossFade() { + super.animate(new DrawableCrossFadeFactory()); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder crossFade(int duration) { + super.animate(new DrawableCrossFadeFactory(duration)); + return this; + } + + /** + * {@inheritDoc} + */ + @Deprecated + @Override + public GifRequestBuilder crossFade(Animation animation, int duration) { + super.animate(new DrawableCrossFadeFactory(animation, duration)); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder crossFade(int animationId, int duration) { + super.animate(new DrawableCrossFadeFactory(context, animationId, + duration)); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder dontAnimate() { + super.dontAnimate(); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder animate(int animationId) { + super.animate(animationId); + return this; + } + + /** + * {@inheritDoc} + */ + @Deprecated + @SuppressWarnings("deprecation") + @Override + public GifRequestBuilder animate(Animation animation) { + super.animate(animation); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder animate(ViewPropertyAnimation.Animator animator) { + super.animate(animator); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder placeholder(int resourceId) { + super.placeholder(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder placeholder(Drawable drawable) { + super.placeholder(drawable); + return this; + } + + @Override + public GifRequestBuilder fallback(Drawable drawable) { + super.fallback(drawable); + return this; + } + + @Override + public GifRequestBuilder fallback(int resourceId) { + super.fallback(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder error(int resourceId) { + super.error(resourceId); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder error(Drawable drawable) { + super.error(drawable); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder listener( + RequestListener requestListener) { + super.listener(requestListener); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder skipMemoryCache(boolean skip) { + super.skipMemoryCache(skip); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder diskCacheStrategy(DiskCacheStrategy strategy) { + super.diskCacheStrategy(strategy); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder override(int width, int height) { + super.override(width, height); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder sourceEncoder(Encoder sourceEncoder) { + super.sourceEncoder(sourceEncoder); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public GifRequestBuilder dontTransform() { + super.dontTransform(); + return this; + } + + @Override + public GifRequestBuilder signature(Key signature) { + super.signature(signature); + return this; + } + + @Override + public GifRequestBuilder load(ModelType model) { + super.load(model); + return this; + } + + @Override + public GifRequestBuilder clone() { + return (GifRequestBuilder) super.clone(); + } + + @Override + void applyFitCenter() { + fitCenter(); + } + + @Override + void applyCenterCrop() { + centerCrop(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/GifTypeRequest.java b/core/src/main/java/com/example/bumptech/glide/GifTypeRequest.java new file mode 100755 index 0000000..a8f99e7 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/GifTypeRequest.java @@ -0,0 +1,81 @@ +package com.example.bumptech.glide; + +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.load.resource.transcode.GifDrawableBytesTranscoder; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.provider.FixedLoadProvider; + +import java.io.InputStream; + +/** + * A class for creating a load request that either loads an {@link GifDrawable} + * directly or that adds an {@link ResourceTranscoder} to transcode + * {@link GifDrawable} into another resource type. + * + * @param The type of model to load the {@link GifDrawable} or other + * transcoded class from. + */ +public class GifTypeRequest extends GifRequestBuilder { + private final ModelLoader streamModelLoader; + private final RequestManager.OptionsApplier optionsApplier; + + private static FixedLoadProvider buildProvider(Glide glide, + ModelLoader streamModelLoader, Class transcodeClass, + ResourceTranscoder transcoder) { + if (streamModelLoader == null) { + return null; + } + + if (transcoder == null) { + transcoder = glide.buildTranscoder(GifDrawable.class, transcodeClass); + } + DataLoadProvider dataLoadProvider = glide.buildDataProvider(InputStream.class, + GifDrawable.class); + return new FixedLoadProvider(streamModelLoader, transcoder, dataLoadProvider); + } + + GifTypeRequest(GenericRequestBuilder other, + ModelLoader streamModelLoader, RequestManager.OptionsApplier optionsApplier) { + super(buildProvider(other.glide, streamModelLoader, GifDrawable.class, null), GifDrawable.class, other); + this.streamModelLoader = streamModelLoader; + this.optionsApplier = optionsApplier; + + // Default to animating. + crossFade(); + } + + /** + * Sets a transcoder to transcode the decoded {@link GifDrawable} into another + * resource type. + * + * @param transcoder The transcoder to use. + * @param transcodeClass The {@link Class} of the resource the + * {@link GifDrawable} will be transcoded to. + * + * @param The type of the resource the {@link GifDrawable} will be + * trasncoded to. + * @return This request builder. + */ + public GenericRequestBuilder transcode( + ResourceTranscoder transcoder, Class transcodeClass) { + FixedLoadProvider provider = buildProvider(glide, streamModelLoader, + transcodeClass, transcoder); + return optionsApplier.apply(new GenericRequestBuilder(provider, + transcodeClass, this)); + } + + /** + * Setup the request to return the bytes of the loaded gif. + *

+ * Note - Any transformations added during this load do not change the underlying bytes and therefore this + * will always load and provide the bytes of the original image before any transformations to the given target. + *

+ * + * @return A new Builder object to build a request to transform the given model into the bytes of an animated gif. + */ + public GenericRequestBuilder toBytes() { + return transcode(new GifDrawableBytesTranscoder(), byte[].class); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/Glide.java b/core/src/main/java/com/example/bumptech/glide/Glide.java new file mode 100755 index 0000000..ee5352c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/Glide.java @@ -0,0 +1,714 @@ +package com.example.bumptech.glide; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.Engine; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.engine.cache.DiskCache; +import com.example.bumptech.glide.load.engine.cache.DiskLruCacheFactory; +import com.example.bumptech.glide.load.engine.cache.MemoryCache; +import com.example.bumptech.glide.load.engine.prefill.BitmapPreFiller; +import com.example.bumptech.glide.load.engine.prefill.PreFillType; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.file_descriptor.FileDescriptorFileLoader; +import com.example.bumptech.glide.load.model.file_descriptor.FileDescriptorResourceLoader; +import com.example.bumptech.glide.load.model.file_descriptor.FileDescriptorStringLoader; +import com.example.bumptech.glide.load.model.file_descriptor.FileDescriptorUriLoader; +import com.example.bumptech.glide.load.model.stream.HttpUrlGlideUrlLoader; +import com.example.bumptech.glide.load.model.stream.StreamByteArrayLoader; +import com.example.bumptech.glide.load.model.stream.StreamFileLoader; +import com.example.bumptech.glide.load.model.stream.StreamResourceLoader; +import com.example.bumptech.glide.load.model.stream.StreamStringLoader; +import com.example.bumptech.glide.load.model.stream.StreamUriLoader; +import com.example.bumptech.glide.load.model.stream.StreamUrlLoader; +import com.example.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.example.bumptech.glide.load.resource.bitmap.FileDescriptorBitmapDataLoadProvider; +import com.example.bumptech.glide.load.resource.bitmap.FitCenter; +import com.example.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable; +import com.example.bumptech.glide.load.resource.bitmap.ImageVideoDataLoadProvider; +import com.example.bumptech.glide.load.resource.bitmap.StreamBitmapDataLoadProvider; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.example.bumptech.glide.load.resource.file.StreamFileDataLoadProvider; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.load.resource.gif.GifDrawableLoadProvider; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapper; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperTransformation; +import com.example.bumptech.glide.load.resource.gifbitmap.ImageVideoGifDrawableLoadProvider; +import com.example.bumptech.glide.load.resource.transcode.GifBitmapWrapperDrawableTranscoder; +import com.example.bumptech.glide.load.resource.transcode.GlideBitmapDrawableTranscoder; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.load.resource.transcode.TranscoderRegistry; +import com.example.bumptech.glide.manager.RequestManagerRetriever; +import com.example.bumptech.glide.module.GlideModule; +import com.example.bumptech.glide.module.ManifestParser; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.provider.DataLoadProviderRegistry; +import com.example.bumptech.glide.request.FutureTarget; +import com.example.bumptech.glide.request.Request; +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.target.ImageViewTargetFactory; +import com.example.bumptech.glide.request.target.Target; +import com.example.bumptech.glide.request.target.ViewTarget; +import com.example.bumptech.glide.util.Util; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.List; + +/** + * A singleton to present a simple static interface for building requests with {@link BitmapRequestBuilder} and + * maintaining an {@link Engine}, {@link BitmapPool}, {@link DiskCache} and + * {@link MemoryCache}. + */ +public class Glide { + + private static final String TAG = "Glide"; + private static volatile Glide glide; + + private final GenericLoaderFactory loaderFactory; + private final Engine engine; + private final BitmapPool bitmapPool; + private final MemoryCache memoryCache; + private final DecodeFormat decodeFormat; + private final ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory(); + private final TranscoderRegistry transcoderRegistry = new TranscoderRegistry(); + private final DataLoadProviderRegistry dataLoadProviderRegistry; + private final CenterCrop bitmapCenterCrop; + private final GifBitmapWrapperTransformation drawableCenterCrop; + private final FitCenter bitmapFitCenter; + private final GifBitmapWrapperTransformation drawableFitCenter; + private final Handler mainHandler; + private final BitmapPreFiller bitmapPreFiller; + private final DiskCache.Factory diskCacheFactory; + + /** + * Returns a directory with a default name in the private cache directory of the application to use to store + * retrieved media and thumbnails. + * + * @see #getPhotoCacheDir(Context, String) + * + * @param context A context. + */ + public static File getPhotoCacheDir(Context context) { + return getPhotoCacheDir(context, DiskLruCacheFactory.DEFAULT_DISK_CACHE_DIR); + } + + /** + * Returns a directory with the given name in the private cache directory of the application to use to store + * retrieved media and thumbnails. + * + * @see #getPhotoCacheDir(Context) + * + * @param context A context. + * @param cacheName The name of the subdirectory in which to store the cache. + */ + public static File getPhotoCacheDir(Context context, String cacheName) { + File cacheDir = context.getCacheDir(); + if (cacheDir != null) { + File result = new File(cacheDir, cacheName); + if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) { + // File wasn't able to create a directory, or the result exists but not a directory + return null; + } + return result; + } + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "default disk cache dir is null"); + } + return null; + } + + /** + * Get the singleton. + * + * @return the singleton + */ + public static Glide get(Context context) { + if (glide == null) { + synchronized (Glide.class) { + if (glide == null) { + Context applicationContext = context.getApplicationContext(); + List modules = new ManifestParser(applicationContext).parse(); + + GlideBuilder builder = new GlideBuilder(applicationContext); + for (GlideModule module : modules) { + module.applyOptions(applicationContext, builder); + } + glide = builder.createGlide(); + for (GlideModule module : modules) { + module.registerComponents(applicationContext, glide); + } + } + } + } + + return glide; + } + + /** + * Returns false if the {@link Glide} singleton has not yet been created and can therefore be setup using + * {@link #setup(GlideBuilder)}. + * + * @see #setup(GlideBuilder) + * + * @deprecated Use {@link GlideModule} instead. Scheduled to be removed in Glide 4.0. + */ + @Deprecated + public static boolean isSetup() { + return glide != null; + } + + /** + * Creates the {@link Glide} singleton using the given builder. Can be used to set options like cache sizes and + * locations. + * + * @see #isSetup() + * + * @deprecated Use {@link GlideModule} instead. Scheduled to be removed in Glide 4.0. + * @param builder The builder. + * @throws IllegalArgumentException if the Glide singleton has already been created. + */ + @Deprecated + public static void setup(GlideBuilder builder) { + if (isSetup()) { + throw new IllegalArgumentException("Glide is already setup, check with isSetup() first"); + } + + glide = builder.createGlide(); + } + + // For testing. + static void tearDown() { + glide = null; + } + + Glide(Engine engine, MemoryCache memoryCache, BitmapPool bitmapPool, Context context, DecodeFormat decodeFormat, DiskCache.Factory diskCacheFactory) { + this.engine = engine; + this.bitmapPool = bitmapPool; + this.memoryCache = memoryCache; + this.decodeFormat = decodeFormat; + this.diskCacheFactory = diskCacheFactory; + loaderFactory = new GenericLoaderFactory(context); + mainHandler = new Handler(Looper.getMainLooper()); + bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat); + + dataLoadProviderRegistry = new DataLoadProviderRegistry(); + + StreamBitmapDataLoadProvider streamBitmapLoadProvider = + new StreamBitmapDataLoadProvider(bitmapPool, decodeFormat); + dataLoadProviderRegistry.register(InputStream.class, Bitmap.class, streamBitmapLoadProvider); + + FileDescriptorBitmapDataLoadProvider fileDescriptorLoadProvider = + new FileDescriptorBitmapDataLoadProvider(bitmapPool, decodeFormat); + dataLoadProviderRegistry.register(ParcelFileDescriptor.class, Bitmap.class, fileDescriptorLoadProvider); + + ImageVideoDataLoadProvider imageVideoDataLoadProvider = + new ImageVideoDataLoadProvider(streamBitmapLoadProvider, fileDescriptorLoadProvider); + dataLoadProviderRegistry.register(ImageVideoWrapper.class, Bitmap.class, imageVideoDataLoadProvider); + + GifDrawableLoadProvider gifDrawableLoadProvider = + new GifDrawableLoadProvider(context, bitmapPool); + dataLoadProviderRegistry.register(InputStream.class, GifDrawable.class, gifDrawableLoadProvider); + + dataLoadProviderRegistry.register(ImageVideoWrapper.class, GifBitmapWrapper.class, + new ImageVideoGifDrawableLoadProvider(imageVideoDataLoadProvider, gifDrawableLoadProvider, bitmapPool)); + + dataLoadProviderRegistry.register(InputStream.class, File.class, new StreamFileDataLoadProvider()); + + register(File.class, ParcelFileDescriptor.class, new FileDescriptorFileLoader.Factory()); + register(File.class, InputStream.class, new StreamFileLoader.Factory()); + register(int.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory()); + register(int.class, InputStream.class, new StreamResourceLoader.Factory()); + register(Integer.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory()); + register(Integer.class, InputStream.class, new StreamResourceLoader.Factory()); + register(String.class, ParcelFileDescriptor.class, new FileDescriptorStringLoader.Factory()); + register(String.class, InputStream.class, new StreamStringLoader.Factory()); + register(Uri.class, ParcelFileDescriptor.class, new FileDescriptorUriLoader.Factory()); + register(Uri.class, InputStream.class, new StreamUriLoader.Factory()); + register(URL.class, InputStream.class, new StreamUrlLoader.Factory()); + register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory()); + register(byte[].class, InputStream.class, new StreamByteArrayLoader.Factory()); + + transcoderRegistry.register(Bitmap.class, GlideBitmapDrawable.class, + new GlideBitmapDrawableTranscoder(context.getResources(), bitmapPool)); + transcoderRegistry.register(GifBitmapWrapper.class, GlideDrawable.class, + new GifBitmapWrapperDrawableTranscoder( + new GlideBitmapDrawableTranscoder(context.getResources(), bitmapPool))); + + bitmapCenterCrop = new CenterCrop(bitmapPool); + drawableCenterCrop = new GifBitmapWrapperTransformation(bitmapPool, bitmapCenterCrop); + + bitmapFitCenter = new FitCenter(bitmapPool); + drawableFitCenter = new GifBitmapWrapperTransformation(bitmapPool, bitmapFitCenter); + } + + /** + * Returns the {@link BitmapPool} used to temporarily store + * {@link Bitmap}s so they can be reused to avoid garbage collections. + * + *

+ * Note - Using this pool directly can lead to undefined behavior and strange drawing errors. Any + * {@link Bitmap} added to the pool must not be currently in use in any other part of the + * application. Any {@link Bitmap} added to the pool must be removed from the pool before it + * is added a second time. + *

+ * + *

+ * Note - To make effective use of the pool, any {@link Bitmap} removed from the pool must + * eventually be re-added. Otherwise the pool will eventually empty and will not serve any useful purpose. + *

+ * + *

+ * The primary reason this object is exposed is for use in custom + * {@link com.bumptech.glide.load.ResourceDecoder}s and {@link com.bumptech.glide.load.Transformation}s. Use + * outside of these classes is not generally recommended. + *

+ */ + public BitmapPool getBitmapPool() { + return bitmapPool; + } + + ResourceTranscoder buildTranscoder(Class decodedClass, Class transcodedClass) { + return transcoderRegistry.get(decodedClass, transcodedClass); + } + + DataLoadProvider buildDataProvider(Class dataClass, Class decodedClass) { + return dataLoadProviderRegistry.get(dataClass, decodedClass); + } + + Target buildImageViewTarget(ImageView imageView, Class transcodedClass) { + return imageViewTargetFactory.buildTarget(imageView, transcodedClass); + } + + Engine getEngine() { + return engine; + } + + CenterCrop getBitmapCenterCrop() { + return bitmapCenterCrop; + } + + FitCenter getBitmapFitCenter() { + return bitmapFitCenter; + } + + GifBitmapWrapperTransformation getDrawableCenterCrop() { + return drawableCenterCrop; + } + + GifBitmapWrapperTransformation getDrawableFitCenter() { + return drawableFitCenter; + } + + Handler getMainHandler() { + return mainHandler; + } + + DecodeFormat getDecodeFormat() { + return decodeFormat; + } + + private GenericLoaderFactory getLoaderFactory() { + return loaderFactory; + } + + /** + * Pre-fills the {@link BitmapPool} using the given sizes. + * + *

+ * Enough Bitmaps are added to completely fill the pool, so most or all of the Bitmaps currently in the pool will + * be evicted. Bitmaps are allocated according to the weights of the given sizes, where each size gets + * (weight / prefillWeightSum) percent of the pool to fill. + *

+ * + *

+ * Note - Pre-filling is done asynchronously using and {@link android.os.MessageQueue.IdleHandler}. Any + * currently running pre-fill will be cancelled and replaced by a call to this method. + *

+ * + *

+ * This method should be used with caution, overly aggressive pre-filling is substantially worse than not + * pre-filling at all. Pre-filling should only be started in onCreate to avoid constantly clearing and + * re-filling the {@link BitmapPool}. Rotation should be carefully + * considered as well. It may be worth calling this method only when no saved instance state exists so that + * pre-filling only happens when the Activity is first created, rather than on every rotation. + *

+ * + * @param bitmapAttributeBuilders The list of + * {@link PreFillType.Builder Builders} representing + * individual sizes and configurations of {@link Bitmap}s to be pre-filled. + */ + public void preFillBitmapPool(PreFillType.Builder... bitmapAttributeBuilders) { + bitmapPreFiller.preFill(bitmapAttributeBuilders); + } + + /** + * Clears as much memory as possible. + * + * @see android.content.ComponentCallbacks#onLowMemory() + * @see android.content.ComponentCallbacks2#onLowMemory() + */ + public void clearMemory() { + // Engine asserts this anyway when removing resources, fail faster and consistently + Util.assertMainThread(); + // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687. + memoryCache.clearMemory(); + bitmapPool.clearMemory(); + } + + /** + * Clears some memory with the exact amount depending on the given level. + * + * @see android.content.ComponentCallbacks2#onTrimMemory(int) + */ + public void trimMemory(int level) { + // Engine asserts this anyway when removing resources, fail faster and consistently + Util.assertMainThread(); + // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687. + memoryCache.trimMemory(level); + bitmapPool.trimMemory(level); + } + + /** + * Clears disk cache. + * + *

+ * This method should always be called on a background thread, since it is a blocking call. + *

+ */ + public void clearDiskCache() { + Util.assertBackgroundThread(); + getEngine().clearDiskCache(); + } + + /** + * Adjusts Glide's current and maximum memory usage based on the given {@link MemoryCategory}. + * + *

+ * The default {@link MemoryCategory} is {@link MemoryCategory#NORMAL}. {@link MemoryCategory#HIGH} increases + * Glide's maximum memory usage by up to 50% and {@link MemoryCategory#LOW} decreases Glide's maximum memory + * usage by 50%. This method should be used to temporarily increase or decrease memory useage for a single + * Activity or part of the app. Use {@link GlideBuilder#setMemoryCache(MemoryCache)} to set a permanent + * memory size if you want to change the default. + *

+ */ + public void setMemoryCategory(MemoryCategory memoryCategory) { + // Engine asserts this anyway when removing resources, fail faster and consistently + Util.assertMainThread(); + // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687. + memoryCache.setSizeMultiplier(memoryCategory.getMultiplier()); + bitmapPool.setSizeMultiplier(memoryCategory.getMultiplier()); + } + + public DiskCache.Factory getDiskCacheFactory() { + return diskCacheFactory; + } + + /** + * Cancel any pending loads Glide may have for the target and free any resources (such as {@link Bitmap}s) that may + * have been loaded for the target so they may be reused. + * + * @param target The Target to cancel loads for. + */ + public static void clear(Target target) { + Util.assertMainThread(); + Request request = target.getRequest(); + if (request != null) { + request.clear(); + target.setRequest(null); + } + } + + /** + * Cancel any pending loads Glide may have for the target and free any resources that may have been loaded into + * the target so they may be reused. + * + * @param target The target to cancel loads for. + */ + public static void clear(FutureTarget target) { + target.clear(); + } + + /** + * Cancel any pending loads Glide may have for the view and free any resources that may have been loaded for the + * view. + * + *

+ * Note that this will only work if {@link View#setTag(Object)} is not called on this view outside of Glide. + *

+ * + * @see #clear(Target). + * + * @param view The view to cancel loads and free resources for. + * @throws IllegalArgumentException if an object other than Glide's metadata is set as the view's tag. + */ + public static void clear(View view) { + Target viewTarget = new ClearTarget(view); + clear(viewTarget); + } + + /** + * Use the given factory to build a {@link ModelLoader} for models of the given class. Generally the best use of + * this method is to replace one of the default factories or add an implementation for other similar low level + * models. Typically the {@link RequestManager#using(com.bumptech.glide.load.model.stream.StreamModelLoader)} or + * {@link RequestManager#using(com.bumptech.glide.load.model.file_descriptor.FileDescriptorModelLoader)} syntax is + * preferred because it directly links the model with the ModelLoader being used to load it. Any factory replaced + * by the given factory will have its {@link ModelLoaderFactory#teardown()}} method called. + * + *

+ * Note - If a factory already exists for the given class, it will be replaced. If that factory is not being + * used for any other model class, {@link ModelLoaderFactory#teardown()} + * will be called. + *

+ * + *

+ * Note - The factory must not be an anonymous inner class of an Activity or another object that cannot be + * retained statically. + *

+ * + * @see RequestManager#using(com.bumptech.glide.load.model.file_descriptor.FileDescriptorModelLoader) + * @see RequestManager#using(com.bumptech.glide.load.model.stream.StreamModelLoader) + * + * @param modelClass The model class. + * @param resourceClass The resource class the model loader will translate the model type into. + * @param factory The factory to use. + * @param The type of the model. + * @param the type of the resource. + */ + public void register(Class modelClass, Class resourceClass, ModelLoaderFactory factory) { + ModelLoaderFactory removed = loaderFactory.register(modelClass, resourceClass, factory); + if (removed != null) { + removed.teardown(); + } + } + + /** + * Removes any {@link ModelLoaderFactory} registered for the given model and resource classes if one exists. If a + * {@link ModelLoaderFactory} is removed, its {@link ModelLoaderFactory#teardown()}} method will be called. + * + * @deprecated Use {@link #register(Class, Class, ModelLoaderFactory)} to replace + * a registered loader rather than simply removing it. + * @param modelClass The model class. + * @param resourceClass The resource class. + * @param The type of the model. + * @param The type of the resource. + */ + @Deprecated + public void unregister(Class modelClass, Class resourceClass) { + ModelLoaderFactory removed = loaderFactory.unregister(modelClass, resourceClass); + if (removed != null) { + removed.teardown(); + } + } + + /** + * Build a {@link ModelLoader} for the given model class using registered {@link ModelLoaderFactory}s. + * + * @see #buildModelLoader(Object, Class, Context) + * @see #buildStreamModelLoader(Class, Context) + * @see #buildFileDescriptorModelLoader(Class, Context) + * + * @param modelClass The class to get a {@link ModelLoader} for. + * @param resourceClass The resource class to get a {@link ModelLoader} for. + * @param context Any context. + * @param The type of the model. + * @param The type of the resource. + * @return A new {@link ModelLoader} for the given model class. + */ + public static ModelLoader buildModelLoader(Class modelClass, Class resourceClass, + Context context) { + if (modelClass == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unable to load null model, setting placeholder only"); + } + return null; + } + return Glide.get(context).getLoaderFactory().buildModelLoader(modelClass, resourceClass); + } + + /** + * A convenience method to build a {@link ModelLoader} for a given model object using registered + * {@link ModelLoaderFactory}s. + * + * @see #buildModelLoader(Class, Class, Context) + * + * @param model A non null model object whose class we will get a {@link ModelLoader} for. + * @param resourceClass The resource class to get a {@link ModelLoader} for. + * @param context Any context. + * @param The type of the model. + * @param The type of the resource. + * @return A new {@link ModelLoader} for the given model and resource classes, or null if model is null. + */ + @SuppressWarnings("unchecked") + public static ModelLoader buildModelLoader(T model, Class resourceClass, Context context) { + return buildModelLoader(model != null ? (Class) model.getClass() : null, resourceClass, context); + } + + /** + * A method to build a {@link ModelLoader} for the given model that produces {@link InputStream}s using a registered + * factory. + * + * @see #buildModelLoader(Class, Class, Context) + */ + public static ModelLoader buildStreamModelLoader(Class modelClass, Context context) { + return buildModelLoader(modelClass, InputStream.class, context); + } + + /** + * A method to build a {@link ModelLoader} for the given model that produces {@link InputStream}s using a registered + * factory. + * + * @see #buildModelLoader(Object, Class, Context) + */ + public static ModelLoader buildStreamModelLoader(T model, Context context) { + return buildModelLoader(model, InputStream.class, context); + } + + /** + * A method to build a {@link ModelLoader} for the given model class that produces + * {@link ParcelFileDescriptor}s using a registered factory. + * + * @see #buildModelLoader(Class, Class, Context) + */ + public static ModelLoader buildFileDescriptorModelLoader(Class modelClass, + Context context) { + return buildModelLoader(modelClass, ParcelFileDescriptor.class, context); + } + + /** + * A method to build a {@link ModelLoader} for the given model class that produces + * {@link ParcelFileDescriptor}s using a registered factory. + * + * @see #buildModelLoader(Object, Class, Context) + */ + public static ModelLoader buildFileDescriptorModelLoader(T model, Context context) { + return buildModelLoader(model, ParcelFileDescriptor.class, context); + } + + /** + * Begin a load with Glide by passing in a context. + * + *

+ * Any requests started using a context will only have the application level options applied and will not be + * started or stopped based on lifecycle events. In general, loads should be started at the level the result + * will be used in. If the resource will be used in a view in a child fragment, + * the load should be started with {@link #with(android.app.Fragment)}} using that child fragment. Similarly, + * if the resource will be used in a view in the parent fragment, the load should be started with + * {@link #with(android.app.Fragment)} using the parent fragment. In the same vein, if the resource will be used + * in a view in an activity, the load should be started with {@link #with(Activity)}}. + *

+ * + *

+ * This method is appropriate for resources that will be used outside of the normal fragment or activity + * lifecycle (For example in services, or for notification thumbnails). + *

+ * + * @see #with(Activity) + * @see #with(android.app.Fragment) + * @see #with(Fragment) + * @see #with(FragmentActivity) + * + * @param context Any context, will not be retained. + * @return A RequestManager for the top level application that can be used to start a load. + */ + public static RequestManager with(Context context) { + RequestManagerRetriever retriever = RequestManagerRetriever.get(); + return retriever.get(context); + } + + /** + * Begin a load with Glide that will be tied to the given {@link Activity}'s lifecycle and that uses the + * given {@link Activity}'s default options. + * + * @param activity The activity to use. + * @return A RequestManager for the given activity that can be used to start a load. + */ + public static RequestManager with(Activity activity) { + RequestManagerRetriever retriever = RequestManagerRetriever.get(); + return retriever.get(activity); + } + + /** + * Begin a load with Glide that will tied to the give {@link FragmentActivity}'s lifecycle + * and that uses the given {@link FragmentActivity}'s default options. + * + * @param activity The activity to use. + * @return A RequestManager for the given FragmentActivity that can be used to start a load. + */ + public static RequestManager with(FragmentActivity activity) { + RequestManagerRetriever retriever = RequestManagerRetriever.get(); + return retriever.get(activity); + } + + /** + * Begin a load with Glide that will be tied to the given {@link android.app.Fragment}'s lifecycle and that uses + * the given {@link android.app.Fragment}'s default options. + * + * @param fragment The fragment to use. + * @return A RequestManager for the given Fragment that can be used to start a load. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static RequestManager with(android.app.Fragment fragment) { + RequestManagerRetriever retriever = RequestManagerRetriever.get(); + return retriever.get(fragment); + } + + /** + * Begin a load with Glide that will be tied to the given {@link Fragment}'s lifecycle and + * that uses the given {@link Fragment}'s default options. + * + * @param fragment The fragment to use. + * @return A RequestManager for the given Fragment that can be used to start a load. + */ + public static RequestManager with(Fragment fragment) { + RequestManagerRetriever retriever = RequestManagerRetriever.get(); + return retriever.get(fragment); + } + + private static class ClearTarget extends ViewTarget { + public ClearTarget(View view) { + super(view); + } + + @Override + public void onLoadStarted(Drawable placeholder) { + // Do nothing. + } + + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + // Do nothing. + } + + @Override + public void onResourceReady(Object resource, GlideAnimation glideAnimation) { + // Do nothing. + } + + @Override + public void onLoadCleared(Drawable placeholder) { + // Do nothing. + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/GlideBuilder.java b/core/src/main/java/com/example/bumptech/glide/GlideBuilder.java new file mode 100755 index 0000000..8db3744 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/GlideBuilder.java @@ -0,0 +1,192 @@ +package com.example.bumptech.glide; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.Engine; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter; +import com.example.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool; +import com.example.bumptech.glide.load.engine.cache.DiskCache; +import com.example.bumptech.glide.load.engine.cache.ExternalCacheDiskCacheFactory; +import com.example.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; +import com.example.bumptech.glide.load.engine.cache.LruResourceCache; +import com.example.bumptech.glide.load.engine.cache.MemoryCache; +import com.example.bumptech.glide.load.engine.cache.MemorySizeCalculator; +import com.example.bumptech.glide.load.engine.executor.FifoPriorityThreadPoolExecutor; + +import java.util.concurrent.ExecutorService; + +/** + * A builder class for setting default structural classes for Glide to use. + */ +public class GlideBuilder { + private final Context context; + + private Engine engine; + private BitmapPool bitmapPool; + private MemoryCache memoryCache; + private ExecutorService sourceService; + private ExecutorService diskCacheService; + private DecodeFormat decodeFormat; + private DiskCache.Factory diskCacheFactory; + + public GlideBuilder(Context context) { + this.context = context.getApplicationContext(); + } + + /** + * Sets the {@link BitmapPool} implementation to use to store and + * retrieve reused {@link android.graphics.Bitmap}s. + * + * @param bitmapPool The pool to use. + * @return This builder. + */ + public GlideBuilder setBitmapPool(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + return this; + } + + /** + * Sets the {@link MemoryCache} implementation to store + * {@link com.bumptech.glide.load.engine.Resource}s that are not currently in use. + * + * @param memoryCache The cache to use. + * @return This builder. + */ + public GlideBuilder setMemoryCache(MemoryCache memoryCache) { + this.memoryCache = memoryCache; + return this; + } + + /** + * Sets the {@link DiskCache.Factory} implementation to use to construct + * the {@link DiskCache} to use to store + * {@link com.bumptech.glide.load.engine.Resource} data on disk. + * + * @param diskCacheFactory The disk cche factory to use. + * @return This builder. + */ + public GlideBuilder setDiskCache(DiskCache.Factory diskCacheFactory) { + this.diskCacheFactory = diskCacheFactory; + return this; + } + + /** + * Sets the {@link ExecutorService} implementation to use when retrieving + * {@link com.bumptech.glide.load.engine.Resource}s that are not already in the cache. + * + *

+ * Any implementation must order requests based on their {@link Priority} for thumbnail + * requests to work properly. + *

+ * + * @see #setDiskCacheService(ExecutorService) + * @see FifoPriorityThreadPoolExecutor + * + * @param service The ExecutorService to use. + * @return This builder. + */ + public GlideBuilder setResizeService(ExecutorService service) { + this.sourceService = service; + return this; + } + + /** + * Sets the {@link ExecutorService} implementation to use when retrieving + * {@link com.bumptech.glide.load.engine.Resource}s that are currently in cache. + * + *

+ * Any implementation must order requests based on their {@link Priority} for thumbnail + * requests to work properly. + *

+ * + * @see #setResizeService(ExecutorService) + * @see FifoPriorityThreadPoolExecutor + * + * @param service The ExecutorService to use. + * @return This builder. + */ + public GlideBuilder setDiskCacheService(ExecutorService service) { + this.diskCacheService = service; + return this; + } + + /** + * Sets the {@link DecodeFormat} that will be the default format for all the default + * decoders that can change the {@link android.graphics.Bitmap.Config} of the {@link android.graphics.Bitmap}s they + * decode. + * + *

+ * Decode format is always a suggestion, not a requirement. See {@link DecodeFormat} for + * more details. + *

+ * + *

+ * If you instantiate and use a custom decoder, it will use + * {@link DecodeFormat#DEFAULT} as its default. + *

+ * + *

+ * Calls to this method are ignored on KitKat and Lollipop. See #301. + *

+ * + * @param decodeFormat The format to use. + * @return This builder. + */ + public GlideBuilder setDecodeFormat(DecodeFormat decodeFormat) { + this.decodeFormat = decodeFormat; + return this; + } + + // For testing. + GlideBuilder setEngine(Engine engine) { + this.engine = engine; + return this; + } + + Glide createGlide() { + if (sourceService == null) { + final int cores = Math.max(1, Runtime.getRuntime().availableProcessors()); + sourceService = new FifoPriorityThreadPoolExecutor(cores); + } + if (diskCacheService == null) { + diskCacheService = new FifoPriorityThreadPoolExecutor(1); + } + + MemorySizeCalculator calculator = new MemorySizeCalculator(context); + if (bitmapPool == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + int size = calculator.getBitmapPoolSize(); + bitmapPool = new LruBitmapPool(size); + } else { + bitmapPool = new BitmapPoolAdapter(); + } + } + + if (memoryCache == null) { + memoryCache = new LruResourceCache(calculator.getMemoryCacheSize()); + } + + if (diskCacheFactory == null) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ + diskCacheFactory = new ExternalCacheDiskCacheFactory(context); + } else { + diskCacheFactory = new InternalCacheDiskCacheFactory(context); + } + } + + if (engine == null) { + engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService); + } + + if (decodeFormat == null) { + decodeFormat = DecodeFormat.DEFAULT; + } + + return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat, diskCacheFactory); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/bumptech/glide/ListPreloader.java b/core/src/main/java/com/example/bumptech/glide/ListPreloader.java new file mode 100755 index 0000000..1fc96d9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/ListPreloader.java @@ -0,0 +1,317 @@ +package com.example.bumptech.glide; + +import android.widget.AbsListView; + + +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.target.BaseTarget; +import com.example.bumptech.glide.request.target.SizeReadyCallback; +import com.example.bumptech.glide.util.Util; + +import java.util.List; +import java.util.Queue; + +/** + * Loads a few resources ahead in the direction of scrolling in any {@link AbsListView} so that images are in the memory + * cache just before the corresponding view in created in the list. Gives the appearance of an infinitely large image + * cache, depending on scrolling speed, cpu speed, and cache size. + * + *

+ * Must be set using {@link AbsListView#setOnScrollListener(AbsListView.OnScrollListener)}, or have its + * corresponding methods called from another {@link AbsListView.OnScrollListener} to function. + *

+ * + * @param The type of the model being displayed in the list. + */ +public class ListPreloader implements AbsListView.OnScrollListener { + + private final int maxPreload; + private final PreloadTargetQueue preloadTargetQueue; + private final PreloadModelProvider preloadModelProvider; + private final PreloadSizeProvider preloadDimensionProvider; + + private int lastEnd; + private int lastStart; + private int lastFirstVisible; + private int totalItemCount; + + private boolean isIncreasing = true; + + /** + * An implementation of PreloadModelProvider should provide all the models that should be preloaded. + * + * @param The type of the model being preloaded. + */ + public interface PreloadModelProvider { + + /** + * Returns a non null list of all models that need to be loaded for the list to display adapter items in + * positions between {@code start} and {@code end}. + * + *

+ * A list of any size can be returned so there can be multiple models per adapter position. + *

+ * + * @param position The adapter position. + */ + List getPreloadItems(int position); + + /** + * Returns a non null {@link GenericRequestBuilder} for a given item. Must exactly match + * the request used to load the resource in the list. + * + *

+ * The target and context will be provided by the preloader. + *

+ * + * @param item The model to load. + */ + GenericRequestBuilder getPreloadRequestBuilder(U item); + } + + /** + * An implementation of PreloadSizeProvider should provide the size of the view in the list where the resources + * will be displayed. + * + * @param The type of the model the size should be provided for. + */ + public interface PreloadSizeProvider { + + /** + * Returns the size of the view in the list where the resources will be displayed in pixels in the format + * [x, y], or {@code null} if no size is currently available. + * + *

+ * Note - The dimensions returned here must precisely match those of the view in the list. + *

+ * + * @param item A model + */ + int[] getPreloadSize(T item, int adapterPosition, int perItemPosition); + } + + /** + * Constructor for {@link ListPreloader} that requires users to subclass and override + * the {@link #getItems(int, int)} and {@link #getRequestBuilder(Object)} methods. + * + * @deprecated Use {@link #ListPreloader(PreloadModelProvider, + * PreloadSizeProvider, int)} instead. This constructor will be removed in Glide + * 4.0. + * @param maxPreload Maximum number of items to preload. + */ + @Deprecated + public ListPreloader(int maxPreload) { + this.preloadModelProvider = new PreloadModelProvider() { + @Override + public List getPreloadItems(int position) { + return getItems(position, position + 1); + } + + @Override + public GenericRequestBuilder getPreloadRequestBuilder(T item) { + return getRequestBuilder(item); + } + }; + this.preloadDimensionProvider = new PreloadSizeProvider() { + + @Override + public int[] getPreloadSize(T item, int adapterPosition, int perItemPosition) { + return getDimensions(item); + } + }; + this.maxPreload = maxPreload; + preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1); + + } + + /** + * Constructor for {@link ListPreloader} that accepts interfaces for providing the dimensions of + * images to preload, the list of models to preload for a given position, and the request to use to load images. + * + * @param preloadModelProvider Provides models to load and requests capable of loading them. + * @param preloadDimensionProvider Provides the dimensions of images to load. + * @param maxPreload Maximum number of items to preload. + */ + public ListPreloader(PreloadModelProvider preloadModelProvider, + PreloadSizeProvider preloadDimensionProvider, int maxPreload) { + this.preloadModelProvider = preloadModelProvider; + this.preloadDimensionProvider = preloadDimensionProvider; + this.maxPreload = maxPreload; + preloadTargetQueue = new PreloadTargetQueue(maxPreload + 1); + } + + @Override + public void onScrollStateChanged(AbsListView absListView, int scrollState) { + // Do nothing. + } + + @Override + public void onScroll(AbsListView absListView, int firstVisible, int visibleCount, + int totalCount) { + totalItemCount = totalCount; + if (firstVisible > lastFirstVisible) { + preload(firstVisible + visibleCount, true); + } else if (firstVisible < lastFirstVisible) { + preload(firstVisible, false); + } + lastFirstVisible = firstVisible; + } + + /** + * Returns the size of the view in the list where the resources will be displayed. + * + *

+ * Note - The size returned here must precisely match those of the view in the list. + *

+ * + * @deprecated Use {@link PreloadSizeProvider} instead. This method will be removed + * in Glide 4.0. + * @param item A model + * @return The size of the view where the item will be displayed + */ + @Deprecated + protected int[] getDimensions(T item) { + throw new IllegalStateException("You must either provide a PreloadDimensionProvider or override " + + "getDimensions()"); + } + + /** + * Returns a non null list of all models that need to be loaded for the list to display adapter items + * between {@code start} and {@code end}. + * + *

+ * A list of any size can be returned so there can be multiple models per adapter position. + *

+ * + * @deprecated Use {@link PreloadModelProvider} instead. This method will be + * removed in Glide 4.0. + * @param start The smallest adapter position. Will be {@code >= 0 && < adapter.getCount() && + * <= end} + * @param end The largest adapter position. Will be {@code >= 0 && < adapter.getCount && >= + * start} + */ + @Deprecated + protected List getItems(int start, int end) { + throw new IllegalStateException("You must either provide a PreloadModelProvider or override getItems()"); + } + + /** + * Returns a non null {@link GenericRequestBuilder} for a given item. Must exactly match the + * request used to load the resource in the list. + * + *

+ * The target and context will be provided by the preloader. + *

+ * + * @deprecated Use {@link PreloadModelProvider} instead. This method will be + * removed in Glide 4.0. + * @param item The model to load. + */ + @SuppressWarnings("rawtypes") + @Deprecated + protected GenericRequestBuilder getRequestBuilder(T item) { + throw new IllegalStateException("You must either provide a PreloadModelProvider, or override " + + "getRequestBuilder()"); + } + + private void preload(int start, boolean increasing) { + if (isIncreasing != increasing) { + isIncreasing = increasing; + cancelAll(); + } + preload(start, start + (increasing ? maxPreload : -maxPreload)); + } + + private void preload(int from, int to) { + int start; + int end; + if (from < to) { + start = Math.max(lastEnd, from); + end = to; + } else { + start = to; + end = Math.min(lastStart, from); + } + end = Math.min(totalItemCount, end); + start = Math.min(totalItemCount, Math.max(0, start)); + + if (from < to) { + // Increasing + for (int i = start; i < end; i++) { + preloadAdapterPosition(this.preloadModelProvider.getPreloadItems(i), i, true); + } + } else { + // Decreasing + for (int i = end - 1; i >= start; i--) { + preloadAdapterPosition(this.preloadModelProvider.getPreloadItems(i), i, false); + } + } + + lastStart = start; + lastEnd = end; + } + + private void preloadAdapterPosition(List items, int position, boolean isIncreasing) { + final int numItems = items.size(); + if (isIncreasing) { + for (int i = 0; i < numItems; ++i) { + preloadItem(items.get(i), position, i); + } + } else { + for (int i = numItems - 1; i >= 0; --i) { + preloadItem(items.get(i), position, i); + } + } + } + + @SuppressWarnings("unchecked") + private void preloadItem(T item, int position, int i) { + final int[] dimensions = this.preloadDimensionProvider.getPreloadSize(item, position, i); + if (dimensions != null) { + GenericRequestBuilder preloadRequestBuilder = this.preloadModelProvider.getPreloadRequestBuilder(item); + preloadRequestBuilder.into(preloadTargetQueue.next(dimensions[0], dimensions[1])); + } + } + + private void cancelAll() { + for (int i = 0; i < maxPreload; i++) { + Glide.clear(preloadTargetQueue.next(0, 0)); + } + } + + private static final class PreloadTargetQueue { + private final Queue queue; + + public PreloadTargetQueue(int size) { + queue = Util.createQueue(size); + + for (int i = 0; i < size; i++) { + queue.offer(new PreloadTarget()); + } + } + + public PreloadTarget next(int width, int height) { + final PreloadTarget result = queue.poll(); + queue.offer(result); + result.photoWidth = width; + result.photoHeight = height; + return result; + } + } + + private static class PreloadTarget extends BaseTarget { + private int photoHeight; + private int photoWidth; + + @Override + public void onResourceReady(Object resource, + GlideAnimation glideAnimation) { + // Do nothing. + } + + @Override + public void getSize(SizeReadyCallback cb) { + cb.onSizeReady(photoWidth, photoHeight); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/MemoryCategory.java b/core/src/main/java/com/example/bumptech/glide/MemoryCategory.java new file mode 100755 index 0000000..cf4e7dd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/MemoryCategory.java @@ -0,0 +1,33 @@ +package com.example.bumptech.glide; + +/** + * An enum for dynamically modifying the amount of memory Glide is able to use. + */ +public enum MemoryCategory { + /** + * Tells Glide's memory cache and bitmap pool to use at most half of their initial maximum size. + */ + LOW(0.5f), + /** + * Tells Glide's memory cache and bitmap pool to use at most their initial maximum size. + */ + NORMAL(1f), + /** + * Tells Glide's memory cache and bitmap pool to use at most one and a half times their initial maximum size. + */ + HIGH(1.5f); + + private float multiplier; + + MemoryCategory(float multiplier) { + this.multiplier = multiplier; + } + + /** + * Returns the multiplier that should be applied to the initial maximum size of Glide's memory cache and bitmap + * pool. + */ + public float getMultiplier() { + return multiplier; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/Priority.java b/core/src/main/java/com/example/bumptech/glide/Priority.java new file mode 100755 index 0000000..c26ca2a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/Priority.java @@ -0,0 +1,13 @@ +package com.example.bumptech.glide; + +/** + * Priorities for completing loads. If more than one load is queued at a time, the load with the higher priority will be + * started first. Priorities are considered best effort, there are no guarantees about the order in which loads will + * start or finish. + */ +public enum Priority { + IMMEDIATE, + HIGH, + NORMAL, + LOW, priority, +} diff --git a/core/src/main/java/com/example/bumptech/glide/RequestManager.java b/core/src/main/java/com/example/bumptech/glide/RequestManager.java new file mode 100755 index 0000000..acc1873 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/RequestManager.java @@ -0,0 +1,804 @@ +package com.example.bumptech.glide; + +import android.content.Context; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.file_descriptor.FileDescriptorModelLoader; +import com.example.bumptech.glide.load.model.stream.MediaStoreStreamLoader; +import com.example.bumptech.glide.load.model.stream.StreamByteArrayLoader; +import com.example.bumptech.glide.load.model.stream.StreamModelLoader; +import com.example.bumptech.glide.manager.ConnectivityMonitor; +import com.example.bumptech.glide.manager.ConnectivityMonitorFactory; +import com.example.bumptech.glide.manager.Lifecycle; +import com.example.bumptech.glide.manager.LifecycleListener; +import com.example.bumptech.glide.manager.RequestManagerTreeNode; +import com.example.bumptech.glide.manager.RequestTracker; +import com.example.bumptech.glide.signature.ApplicationVersionSignature; +import com.example.bumptech.glide.signature.MediaStoreSignature; +import com.example.bumptech.glide.signature.StringSignature; +import com.example.bumptech.glide.util.Util; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.util.UUID; + +/** + * A class for managing and starting requests for Glide. Can use activity, fragment and connectivity lifecycle events to + * intelligently stop, start, and restart requests. Retrieve either by instantiating a new object, or to take advantage + * built in Activity and Fragment lifecycle handling, use the static Glide.load methods with your Fragment or Activity. + * + * @see Glide#with(android.app.Activity) + * @see Glide#with(android.support.v4.app.FragmentActivity) + * @see Glide#with(android.app.Fragment) + * @see Glide#with(android.support.v4.app.Fragment) + * @see Glide#with(Context) + */ +public class RequestManager implements LifecycleListener { + private final Context context; + private final Lifecycle lifecycle; + private final RequestManagerTreeNode treeNode; + private final RequestTracker requestTracker; + private final Glide glide; + private final OptionsApplier optionsApplier; + private DefaultOptions options; + + public RequestManager(Context context, Lifecycle lifecycle, RequestManagerTreeNode treeNode) { + this(context, lifecycle, treeNode, new RequestTracker(), new ConnectivityMonitorFactory()); + } + + RequestManager(Context context, final Lifecycle lifecycle, RequestManagerTreeNode treeNode, + RequestTracker requestTracker, ConnectivityMonitorFactory factory) { + this.context = context.getApplicationContext(); + this.lifecycle = lifecycle; + this.treeNode = treeNode; + this.requestTracker = requestTracker; + this.glide = Glide.get(context); + this.optionsApplier = new OptionsApplier(); + + ConnectivityMonitor connectivityMonitor = factory.build(context, + new RequestManagerConnectivityListener(requestTracker)); + + // If we're the application level request manager, we may be created on a background thread. In that case we + // cannot risk synchronously pausing or resuming requests, so we hack around the issue by delaying adding + // ourselves as a lifecycle listener by posting to the main thread. This should be entirely safe. + if (Util.isOnBackgroundThread()) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + lifecycle.addListener(RequestManager.this); + } + }); + } else { + lifecycle.addListener(this); + } + lifecycle.addListener(connectivityMonitor); + } + + /** + * @see android.content.ComponentCallbacks2#onTrimMemory(int) + */ + public void onTrimMemory(int level) { + glide.trimMemory(level); + } + + /** + * @see android.content.ComponentCallbacks2#onLowMemory() + */ + public void onLowMemory() { + glide.clearMemory(); + } + + /** + * An interface that allows a default set of options to be applied to all requests started from an + * {@link RequestManager}. + */ + public interface DefaultOptions { + /** + * Allows the implementor to apply some options to the given request. + * + * @param requestBuilder The request builder being used to construct the load. + * @param The type of the model. + */ + void apply(GenericRequestBuilder requestBuilder); + } + + /** + * Sets an interface that can apply some default options to all Requests started using this {@link RequestManager}. + * + *

+ * Note - These options will be retained for the life the of this {@link RequestManager} + * so be wary of using + * {@link GenericRequestBuilder#listener(com.bumptech.glide.request.RequestListener)}} when + * starting requests using an {@link Context} or {@link android.app.Application} to avoid + * leaking memory. Any option that does not use an anonymous inner class is generally safe. + *

+ * + * @param options The default options to apply to all requests. + */ + public void setDefaultOptions(DefaultOptions options) { + this.options = options; + } + + /** + * Returns true if loads for this {@link RequestManager} are currently paused. + * + * @see #pauseRequests() + * @see #resumeRequests() + */ + public boolean isPaused() { + Util.assertMainThread(); + return requestTracker.isPaused(); + } + + /** + * Cancels any in progress loads, but does not clear resources of completed loads. + * + * @see #isPaused() + * @see #resumeRequests() + */ + public void pauseRequests() { + Util.assertMainThread(); + requestTracker.pauseRequests(); + } + + /** + * Performs {@link #pauseRequests()} recursively for all managers that are contextually descendant + * to this manager based on the Activity/Fragment hierarchy: + * + *
    + *
  • When pausing on an Activity all attached fragments will also get paused. + *
  • When pausing on an attached Fragment all descendant fragments will also get paused. + *
  • When pausing on a detached Fragment or the application context only the current RequestManager is paused. + *
+ * + *

Note, on pre-Jelly Bean MR1 calling pause on a Fragment will not cause child fragments to pause, in this + * case either call pause on the Activity or use a support Fragment. + */ + public void pauseRequestsRecursive() { + Util.assertMainThread(); + pauseRequests(); + for (RequestManager requestManager : treeNode.getDescendants()) { + requestManager.pauseRequests(); + } + } + + /** + * Restarts any loads that have not yet completed. + * + * @see #isPaused() + * @see #pauseRequests() + */ + public void resumeRequests() { + Util.assertMainThread(); + requestTracker.resumeRequests(); + } + + /** + * Performs {@link #resumeRequests()} recursively for all managers that are contextually descendant + * to this manager based on the Activity/Fragment hierarchy. The hierarchical semantics are identical as for + * {@link #pauseRequestsRecursive()}. + */ + public void resumeRequestsRecursive() { + Util.assertMainThread(); + resumeRequests(); + for (RequestManager requestManager : treeNode.getDescendants()) { + requestManager.resumeRequests(); + } + } + + /** + * Lifecycle callback that registers for connectivity events (if the android.permission.ACCESS_NETWORK_STATE + * permission is present) and restarts failed or paused requests. + */ + @Override + public void onStart() { + // onStart might not be called because this object may be created after the fragment/activity's onStart method. + resumeRequests(); + } + + /** + * Lifecycle callback that unregisters for connectivity events (if the android.permission.ACCESS_NETWORK_STATE + * permission is present) and pauses in progress loads. + */ + @Override + public void onStop() { + pauseRequests(); + } + + /** + * Lifecycle callback that cancels all in progress requests and clears and recycles resources for all completed + * requests. + */ + @Override + public void onDestroy() { + requestTracker.clearRequests(); + } + + /** + * Returns a request builder that uses the given {@link ModelLoader} to fetch a + * generic data type. + * + *

+ * Warning - This is an experimental api that may change without a change in major version. + *

+ * + * @param modelLoader The {@link ModelLoader} class to use to load the model. + * @param dataClass The type of data the {@link ModelLoader} will load. + * @param The type of the model to be loaded. + * @param The type of the data to be loaded from the mode. + */ + public GenericModelRequest using(ModelLoader modelLoader, Class dataClass) { + return new GenericModelRequest(modelLoader, dataClass); + } + + /** + * Returns a request builder that uses the given {@link StreamModelLoader} to + * fetch an {@link InputStream} for loading images. + * + * @param modelLoader The model loader to use. + * @param The type of the model. + */ + public ImageModelRequest using(final StreamModelLoader modelLoader) { + return new ImageModelRequest(modelLoader); + } + + /** + * Returns a request builder that uses the given + * {@link StreamByteArrayLoader} to fetch an {@link InputStream} for + * loading Bitmaps. + * + * @param modelLoader The byte array loader. + */ + public ImageModelRequest using(StreamByteArrayLoader modelLoader) { + return new ImageModelRequest(modelLoader); + } + + /** + * Returns a new request builder that uses the given {@link ModelLoader} to fetch a + * {@link ParcelFileDescriptor} for loading video thumbnails. + * + * @param modelLoader The model loader to use. + * @param The type of the model. + */ + public VideoModelRequest using(final FileDescriptorModelLoader modelLoader) { + return new VideoModelRequest(modelLoader); + } + + /** + * Returns a request builder to load the given {@link String}. + * signature. + * + * @see #fromString() + * @see #load(Object) + * + * @param string A file path, or a uri or url handled by {@link com.bumptech.glide.load.model.UriLoader}. + */ + public DrawableTypeRequest load(String string) { + return (DrawableTypeRequest) fromString().load(string); + } + + /** + * Returns a request builder that loads data from {@link String}s using an empty signature. + * + *

+ * Note - this method caches data using only the given String as the cache key. If the data is a Uri outside of + * your control, or you otherwise expect the data represented by the given String to change without the String + * identifier changing, Consider using + * {@link GenericRequestBuilder#signature(Key)} to mixin a signature + * you create that identifies the data currently at the given String that will invalidate the cache if that data + * changes. Alternatively, using {@link DiskCacheStrategy#NONE} and/or + * {@link DrawableRequestBuilder#skipMemoryCache(boolean)} may be appropriate. + *

+ * + * @see #from(Class) + * @see #load(String) + */ + public DrawableTypeRequest fromString() { + return loadGeneric(String.class); + } + + /** + * Returns a request builder to load the given {@link Uri}. + * + * @see #fromUri() + * @see #load(Object) + * + * @param uri The Uri representing the image. Must be of a type handled by + * {@link com.bumptech.glide.load.model.UriLoader}. + */ + public DrawableTypeRequest load(Uri uri) { + return (DrawableTypeRequest) fromUri().load(uri); + } + + /** + * Returns a request builder to load data from {@link Uri}s using no signature. + * + *

+ * Note - this method caches data at Uris using only the Uri itself as the cache key. The data represented by + * Uris from some content providers may change without the Uri changing, which means using this method + * can lead to displaying stale data. Consider using + * {@link GenericRequestBuilder#signature(Key)} to mixin a signature + * you create based on the data at the given Uri that will invalidate the cache if that data changes. + * Alternatively, using {@link DiskCacheStrategy#NONE} and/or + * {@link DrawableRequestBuilder#skipMemoryCache(boolean)} may be appropriate. + *

+ * + * @see #from(Class) + * @see #loadFromMediaStore(Uri) + * @see #loadFromMediaStore(Uri, String, long, int) + * @see GenericRequestBuilder#signature(Key) + */ + public DrawableTypeRequest fromUri() { + return loadGeneric(Uri.class); + } + + /** + * Returns a request builder that uses {@link android.provider.MediaStore.Images.Thumbnails} and + * {@link android.provider.MediaStore.Video.Thumbnails} to retrieve pre-generated thumbnails for the given uri if + * available and uses the given additional data to build a unique signature for cache invalidation. + * + * @see #loadFromMediaStore(Uri) + * @see #load(Uri) + * @see GenericRequestBuilder#signature(Key) + * @see MediaStoreSignature + * + * @deprecated Use {@link #loadFromMediaStore(Uri)}, + * {@link MediaStoreSignature}, and + * {@link DrawableRequestBuilder#signature(Key)} instead. Scheduled to be + * removed in Glide 4.0. + * @param uri The uri representing the media. + * @param mimeType The mime type of the media store media. Ok to default to empty string "". See + * {@link android.provider.MediaStore.Images.ImageColumns#MIME_TYPE} or + * {@link android.provider.MediaStore.Video.VideoColumns#MIME_TYPE}. + * @param dateModified The date modified time of the media store media. Ok to default to 0. See + * {@link android.provider.MediaStore.Images.ImageColumns#DATE_MODIFIED} or + * {@link android.provider.MediaStore.Video.VideoColumns#DATE_MODIFIED}. + * @param orientation The orientation of the media store media. Ok to default to 0. See + * {@link android.provider.MediaStore.Images.ImageColumns#ORIENTATION}. + */ + @Deprecated + public DrawableTypeRequest loadFromMediaStore(Uri uri, String mimeType, long dateModified, int orientation) { + Key signature = new MediaStoreSignature(mimeType, dateModified, orientation); + return (DrawableTypeRequest) loadFromMediaStore(uri).signature(signature); + } + + /** + * Returns a request builder to load the given media store {@link Uri}. + * + * @see #fromMediaStore() + * @see #load(Object) + * + * @param uri The uri representing the media. + */ + public DrawableTypeRequest loadFromMediaStore(Uri uri) { + return (DrawableTypeRequest) fromMediaStore().load(uri); + } + + /** + * Returns a request builder that uses {@link android.provider.MediaStore.Images.Thumbnails} and + * {@link android.provider.MediaStore.Video.Thumbnails} to retrieve pre-generated thumbnails for + * {@link Uri}s. + * + *

+ * Falls back to the registered {@link com.bumptech.glide.load.model.ModelLoaderFactory} registered for + * {@link Uri}s if the given uri is not a media store uri or if no pre-generated thumbnail exists for the given + * uri. + *

+ * + *

+ * Note - This method by default caches data using the given Uri as the key. Since content in the media store + * can change at any time, you should use + * {@link GenericRequestBuilder#signature(Key)} to mix in some + * additional data identifying the current state of the Uri, preferably using + * {@link MediaStoreSignature}. Alternatively consider avoiding the memory and + * disk caches entirely using + * {@link GenericRequestBuilder#diskCacheStrategy(DiskCacheStrategy)} + * and {@link DiskCacheStrategy#NONE} and/or + * {@link GenericRequestBuilder#skipMemoryCache(boolean)}. + *

+ * + * @see #from(Class) + * @see #loadFromMediaStore(Uri, String, long, int) + * @see #load(Uri) + * @see MediaStoreSignature + */ + public DrawableTypeRequest fromMediaStore() { + ModelLoader genericStreamLoader = Glide.buildStreamModelLoader(Uri.class, context); + ModelLoader mediaStoreLoader = new MediaStoreStreamLoader(context, genericStreamLoader); + ModelLoader fileDescriptorModelLoader = + Glide.buildFileDescriptorModelLoader(Uri.class, context); + + return optionsApplier.apply(new DrawableTypeRequest(Uri.class, mediaStoreLoader, + fileDescriptorModelLoader, context, glide, requestTracker, lifecycle, optionsApplier)); + } + + /** + * Returns a request builder to load the given {@link File}. + * + * @see #fromFile() + * @see #load(Object) + * + * @param file The File containing the image + */ + public DrawableTypeRequest load(File file) { + return (DrawableTypeRequest) fromFile().load(file); + } + + /** + * Returns a request builder that uses the {@link com.bumptech.glide.load.model.ModelLoaderFactory} currently + * registered for {@link File} to load the image represented by the given {@link File}. Defaults to + * {@link com.bumptech.glide.load.model.stream.StreamFileLoader.Factory} and + * {@link com.bumptech.glide.load.model.stream.StreamFileLoader} to load images from {@link File}s. + * + *

+ * Note - this method caches data for Files using only the file path itself as the cache key. The data in the + * File can change so using this method can lead to displaying stale data. If you expect the data in the File to + * change, Consider using + * {@link GenericRequestBuilder#signature(Key)} to mixin a signature + * you create that identifies the data currently in the File that will invalidate the cache if that data + * changes. Alternatively, using {@link DiskCacheStrategy#NONE} and/or + * {@link DrawableRequestBuilder#skipMemoryCache(boolean)} may be appropriate. + *

+ * + * @see #load(File) + * @see #from(Class) + */ + public DrawableTypeRequest fromFile() { + return loadGeneric(File.class); + } + + /** + * Returns a request builder to load the given resource id. + * + * @see #fromResource() + * @see #load(Object) + * + * @param resourceId the id of the resource containing the image + */ + public DrawableTypeRequest load(Integer resourceId) { + return (DrawableTypeRequest) fromResource().load(resourceId); + } + + /** + * Returns a request builder that uses the {@link com.bumptech.glide.load.model.ModelLoaderFactory} currently + * registered for {@link Integer} to load the image represented by the given {@link Integer} resource id. Defaults + * to {@link com.bumptech.glide.load.model.stream.StreamResourceLoader.Factory} and + * {@link com.bumptech.glide.load.model.stream.StreamResourceLoader} to load resource id models. + * + *

+ * By default this method adds a version code based signature to the cache key used to cache this resource in + * Glide. This signature is sufficient to guarantee that end users will see the most up to date versions of + * your Drawables, but during development if you do not increment your version code before each install and + * you replace a Drawable with different data without changing the Drawable name, you may see inconsistent + * cached data. To get around this, consider using {@link DiskCacheStrategy#NONE} + * via {@link GenericRequestBuilder#diskCacheStrategy(DiskCacheStrategy)} + * during development, and re-enabling the default + * {@link DiskCacheStrategy#RESULT} for release builds. + *

+ * + * @see #from(Class) + * @see #load(Integer) + * @see ApplicationVersionSignature + * @see GenericRequestBuilder#signature(Key) + */ + public DrawableTypeRequest fromResource() { + return (DrawableTypeRequest) loadGeneric(Integer.class) + .signature(ApplicationVersionSignature.obtain(context)); + } + + /** + * Returns a request builder to load the given {@link URL}. + * + * @see #fromUrl() + * @see #load(Object) + * + * @deprecated The {@link URL} class has + *
a number of performance problems and should generally be avoided when + * possible. Prefer {@link #load(Uri)} or {@link #load(String)}. + * @param url The URL representing the image. + */ + @Deprecated + public DrawableTypeRequest load(URL url) { + return (DrawableTypeRequest) fromUrl().load(url); + } + + /** + * Returns a request builder that uses the {@link com.bumptech.glide.load.model.ModelLoaderFactory} currently + * registered for {@link URL} to load the image represented by the given {@link URL}. Defaults to + * {@link com.bumptech.glide.load.model.stream.HttpUrlGlideUrlLoader} and + * {@link com.bumptech.glide.load.data.HttpUrlFetcher} to load {@link URL} models. + * + * @see #from(Class) + * @see #load(URL) + * + * @deprecated The {@link URL} class has + * a number of performance problems and should generally be avoided when + * possible. Prefer {@link #load(Uri)} or {@link #load(String)}. + */ + @Deprecated + public DrawableTypeRequest fromUrl() { + return loadGeneric(URL.class); + } + + /** + * Returns a request builder that uses a {@link StreamByteArrayLoader} to load an image from the given byte array. + * + * + *

+ * Note - by default loads for bytes are not cached in either the memory or the disk cache. + *

+ * + * @see #load(byte[]) + * + * @deprecated Use {@link #load(byte[])} along with + * {@link GenericRequestBuilder#signature(Key)} instead. Scheduled to be + * removed in Glide 4.0. + * @param model The data to load. + * @param id A unique id that identifies the image represented by the model suitable for use as a cache key + * (url, filepath etc). If there is no suitable id, use {@link #load(byte[])} instead. + */ + @Deprecated + public DrawableTypeRequest load(byte[] model, final String id) { + return (DrawableTypeRequest) load(model).signature(new StringSignature(id)); + } + + /** + * Returns a request to load the given byte array. + * + * @see #fromBytes() + * @see #load(Object) + * + * @param model the data to load. + */ + public DrawableTypeRequest load(byte[] model) { + return (DrawableTypeRequest) fromBytes().load(model); + } + + /** + * Returns a request builder that uses {@link StreamByteArrayLoader} to load + * images from byte arrays. + * + *

+ * Note - by default loads for bytes are not cached in either the memory or the disk cache. + *

+ * + * @see #from(Class) + * @see #load(byte[]) + */ + public DrawableTypeRequest fromBytes() { + return (DrawableTypeRequest) loadGeneric(byte[].class) + .signature(new StringSignature(UUID.randomUUID().toString())) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true /*skipMemoryCache*/); + } + + /** + * Returns a request builder that uses the {@link com.bumptech.glide.load.model.ModelLoaderFactory}s currently + * registered for the given model class for {@link InputStream}s and {@link ParcelFileDescriptor}s to load a + * thumbnail from either the image or the video represented by the given model. + * + *

+ * Note - for maximum efficiency, consider using {@link #from(Class)}} to avoid repeatedly allocating builder + * objects. + *

+ * + * @see #from(Class) + * + * @param model The model the load. + * @param The type of the model to load. + */ + public DrawableTypeRequest load(T model) { + return (DrawableTypeRequest) loadGeneric(getSafeClass(model)).load(model); + } + + /** + * Returns a request builder that can be used for multiple loads that uses the + * {@link com.bumptech.glide.load.model.ModelLoaderFactory}s registered for the given model class for + * {@link InputStream}s and {@link ParcelFileDescriptor}s to load a thumbnail from objects of + * the given modelClass. + * + *

+ * Note - you must use {@link DrawableRequestBuilder#load(Object)}} to set a concrete model + * to be loaded before calling + * {@link DrawableRequestBuilder#into(com.bumptech.glide.request.target.Target)}. You may + * also use this object for repeated loads by calling request.load(model).into(target). You may + * also adjust the options after calling {@link DrawableRequestBuilder#load(Object)}} and/or + * {@link DrawableRequestBuilder#into(com.bumptech.glide.request.target.Target)}}. However, + * keep in mind that any changes in options will apply to all future loads. + *

+ * + * @param modelClass The class of model requests built by this class will load data from. + * @param The type of the model. + */ + public DrawableTypeRequest from(Class modelClass) { + return loadGeneric(modelClass); + } + + private DrawableTypeRequest loadGeneric(Class modelClass) { + ModelLoader streamModelLoader = Glide.buildStreamModelLoader(modelClass, context); + ModelLoader fileDescriptorModelLoader = + Glide.buildFileDescriptorModelLoader(modelClass, context); + if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) { + throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for" + + " which there is a registered ModelLoader, if you are using a custom model, you must first call" + + " Glide#register with a ModelLoaderFactory for your custom model class"); + } + + return optionsApplier.apply( + new DrawableTypeRequest(modelClass, streamModelLoader, fileDescriptorModelLoader, context, + glide, requestTracker, lifecycle, optionsApplier)); + } + + @SuppressWarnings("unchecked") + private static Class getSafeClass(T model) { + return model != null ? (Class) model.getClass() : null; + } + + /** + * A helper class for building requests with custom {@link ModelLoader}s that translate models to + * {@link ParcelFileDescriptor} resources for loading video thumbnails. + * + * @param The type of the model. + */ + public final class VideoModelRequest { + private final ModelLoader loader; + + VideoModelRequest(ModelLoader loader) { + this.loader = loader; + } + + public DrawableTypeRequest load(T model) { + return (DrawableTypeRequest) optionsApplier.apply(new DrawableTypeRequest(getSafeClass(model), null, + loader, context, glide, requestTracker, lifecycle, optionsApplier)) + .load(model); + } + } + + /** + * A helper class for building requests with custom {@link ModelLoader}s that translate models to + * {@link InputStream} resources for loading images. + * + * @param The type of the model. + */ + public final class ImageModelRequest { + private final ModelLoader loader; + + ImageModelRequest(ModelLoader loader) { + this.loader = loader; + } + + /** + * Returns a request builder that uses the provided {@link ModelLoader} to load + * images from an {@link InputStream}s obtained from models of the given model class. + * + * @param modelClass The class of model to load images from. + */ + public DrawableTypeRequest from(Class modelClass) { + return optionsApplier.apply(new DrawableTypeRequest(modelClass, loader, null, context, glide, + requestTracker, lifecycle, optionsApplier)); + } + + /** + * Returns a request builder that uses the provided {@link ModelLoader} to load + * an image from an {@link InputStream} obtained from the given model. + * + * @see #from(Class) + * + * @param model The model to load an image from. + */ + public DrawableTypeRequest load(T model) { + return (DrawableTypeRequest) from(getSafeClass(model)).load(model); + } + } + + /** + * A helper class for building requests with custom {@link ModelLoader}s that requires the user to provide a + * specific model. + * + * @param The type of the model. + * @param The type of data the {@link ModelLoader} provides an + * {@link com.bumptech.glide.load.data.DataFetcher} to convert the model to. + */ + public final class GenericModelRequest { + private final ModelLoader modelLoader; + private final Class dataClass; + + GenericModelRequest(ModelLoader modelLoader, Class dataClass) { + this.modelLoader = modelLoader; + this.dataClass = dataClass; + } + + /** + * Sets the type of model that will be loaded. + * + * @param modelClass the class of model to use. + * @return A request builder + */ + public GenericTypeRequest from(Class modelClass) { + return new GenericTypeRequest(modelClass); + } + + /** + * Sets the specific model that will be loaded. + * + * @param model The model to use. + * @return A request builder. + */ + public GenericTypeRequest load(A model) { + return new GenericTypeRequest(model); + } + + /** + * A helper class for building requests with custom {@link ModelLoader}s that + * requires the user to specify a specific resource class that will be loaded. + * + */ + public final class GenericTypeRequest { + private final A model; + private final Class modelClass; + private final boolean providedModel; + + GenericTypeRequest(A model) { + providedModel = true; + this.model = model; + this.modelClass = getSafeClass(model); + } + + GenericTypeRequest(Class modelClass) { + providedModel = false; + this.model = null; + this.modelClass = modelClass; + } + + /** + * Sets the resource class that will be loaded. + * + * @param resourceClass The class of the resource that will be loaded. + * @param The type of the resource that will be loaded. + * @return This request builder. + */ + public GenericTranscodeRequest as(Class resourceClass) { + GenericTranscodeRequest result = + optionsApplier.apply(new GenericTranscodeRequest(context, glide, modelClass, + modelLoader, dataClass, resourceClass, requestTracker, lifecycle, optionsApplier)); + if (providedModel) { + result.load(model); + } + return result; + } + } + } + + class OptionsApplier { + + public > X apply(X builder) { + if (options != null) { + options.apply(builder); + } + return builder; + } + } + + private static class RequestManagerConnectivityListener implements ConnectivityMonitor.ConnectivityListener { + private final RequestTracker requestTracker; + + public RequestManagerConnectivityListener(RequestTracker requestTracker) { + this.requestTracker = requestTracker; + } + + @Override + public void onConnectivityChanged(boolean isConnected) { + if (isConnected) { + requestTracker.restartRequests(); + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/disklrucache/DiskLruCache.java b/core/src/main/java/com/example/bumptech/glide/disklrucache/DiskLruCache.java new file mode 100755 index 0000000..2c9dc42 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/disklrucache/DiskLruCache.java @@ -0,0 +1,875 @@ +/* + * 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.example.bumptech.glide.disklrucache; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Each key must match + * the regex [a-z0-9_-]{1,120}. Values are byte sequences, + * accessible as streams or files. Each value must be between {@code 0} and + * {@code Integer.MAX_VALUE} bytes in length. + * + *

The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + *

This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + *

Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + *

Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + *

This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final File journalFileBackup; + private final int appVersion; + private long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries = + new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /** This cache uses a single background thread to evict entries. */ + final ThreadPoolExecutor executorService = + new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // Closed. + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + return cache; + } catch (IOException journalIsCorrupt) { + System.out + .println("DiskLruCache " + + directory + + " is corrupt: " + + journalIsCorrupt.getMessage() + + ", removing"); + cache.delete(); + } + } + + // Create a new empty cache. + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); + try { + String magic = reader.readLine(); + String version = reader.readLine(); + String appVersionString = reader.readLine(); + String valueCountString = reader.readLine(); + String blank = reader.readLine(); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + + valueCountString + ", " + blank + "]"); + } + + int lineCount = 0; + while (true) { + try { + readJournalLine(reader.readLine()); + lineCount++; + } catch (EOFException endOfJournal) { + break; + } + } + redundantOpCount = lineCount - lruEntries.size(); + + // If we ended on a truncated line, rebuild the journal before appending to it. + if (reader.hasUnterminatedLine()) { + rebuildJournal(); + } else { + journalWriter = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + } finally { + Util.closeQuietly(reader); + } + } + + private void readJournalLine(String line) throws IOException { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException("unexpected journal line: " + line); + } + + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { + entry.currentEditor = new Editor(entry); + } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); + try { + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + } finally { + writer.close(); + } + + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + + private static void deleteIfExists(File file) throws IOException { + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Value get(String key) throws IOException { + checkNotClosed(); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + for (File file : entry.cleanFiles) { + // A file must have been deleted manually! + if (!file.exists()) { + return null; + } + } + + redundantOpCount++; + journalWriter.append(READ); + journalWriter.append(' '); + journalWriter.append(key); + journalWriter.append('\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null + || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // Value is stale. + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // Another edit is in progress. + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // Flush the journal before creating files to prevent file leaks. + journalWriter.append(DIRTY); + journalWriter.append(' '); + journalWriter.append(key); + journalWriter.append('\n'); + journalWriter.flush(); + return editor; + } + + /** Returns the directory where this cache stores its data. */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public synchronized long getMaxSize() { + return maxSize; + } + + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + this.maxSize = maxSize; + executorService.submit(cleanupCallable); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // If this edit is creating the entry for the first time, every index must have a value. + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException("Newly created entry didn't create value for index " + i); + } + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.append(CLEAN); + journalWriter.append(' '); + journalWriter.append(entry.key); + journalWriter.append(entry.getLengths()); + journalWriter.append('\n'); + + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.append(REMOVE); + journalWriter.append(' '); + journalWriter.append(entry.key); + journalWriter.append('\n'); + } + journalWriter.flush(); + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold // + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (file.exists() && !file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE); + journalWriter.append(' '); + journalWriter.append(key); + journalWriter.append('\n'); + + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** Returns true if this cache has been closed. */ + public synchronized boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** Force buffered operations to the filesystem. */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** Closes this cache. Stored values will remain on the filesystem. */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // Already closed. + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + Util.deleteContents(directory); + } + + private static String inputStreamToString(InputStream in) throws IOException { + return Util.readFully(new InputStreamReader(in, Util.UTF_8)); + } + + /** A snapshot of the values for an entry. */ + public final class Value { + private final String key; + private final long sequenceNumber; + private final long[] lengths; + private final File[] files; + + private Value(String key, long sequenceNumber, File[] files, long[] lengths) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.files = files; + this.lengths = lengths; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + public File getFile(int index) { + return files[index]; + } + + /** Returns the string value for {@code index}. */ + public String getString(int index) throws IOException { + InputStream is = new FileInputStream(files[index]); + return inputStreamToString(is); + } + + /** Returns the byte length of the value for {@code index}. */ + public long getLength(int index) { + return lengths[index]; + } + } + + /** Edits the values for an entry. */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean committed; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + private InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + try { + return new FileInputStream(entry.getCleanFile(index)); + } catch (FileNotFoundException e) { + return null; + } + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + public File getFile(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + File dirtyFile = entry.getDirtyFile(index); + if (!directory.exists()) { + directory.mkdirs(); + } + return dirtyFile; + } + } + + /** Sets the value at {@code index} to {@code value}. */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + OutputStream os = new FileOutputStream(getFile(index)); + writer = new OutputStreamWriter(os, Util.UTF_8); + writer.write(value); + } finally { + Util.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + // The object using this Editor must catch and handle any errors + // during the write. If there is an error and they call commit + // anyway, we will assume whatever they managed to write was valid. + // Normally they should call abort. + completeEdit(this, true); + committed = true; + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** Memoized File objects for this entry to avoid char[] allocations. */ + File[] cleanFiles; + File[] dirtyFiles; + + /** True if this entry has ever been published. */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + cleanFiles = new File[valueCount]; + dirtyFiles = new File[valueCount]; + + // The names are repetitive so re-use the same builder to avoid allocations. + StringBuilder fileBuilder = new StringBuilder(key).append('.'); + int truncateTo = fileBuilder.length(); + for (int i = 0; i < valueCount; i++) { + fileBuilder.append(i); + cleanFiles[i] = new File(directory, fileBuilder.toString()); + fileBuilder.append(".tmp"); + dirtyFiles[i] = new File(directory, fileBuilder.toString()); + fileBuilder.setLength(truncateTo); + } + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** Set lengths using decimal numbers like "10123". */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return cleanFiles[i]; + } + + public File getDirtyFile(int i) { + return dirtyFiles[i]; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/disklrucache/StrictLineReader.java b/core/src/main/java/com/example/bumptech/glide/disklrucache/StrictLineReader.java new file mode 100755 index 0000000..1856fc0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/disklrucache/StrictLineReader.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2012 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.example.bumptech.glide.disklrucache; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * Buffers input from an {@link InputStream} for reading lines. + * + *

This class is used for buffered reading of lines. For purposes of this class, a line ends + * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated + * line at end of input is invalid and will be ignored, the caller may use {@code + * hasUnterminatedLine()} to detect it after catching the {@code EOFException}. + * + *

This class is intended for reading input that strictly consists of lines, such as line-based + * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction + * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different + * end-of-input reporting and a more restrictive definition of a line. + * + *

This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 + * and 10, respectively, and the representation of no other character contains these values. + * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. + * The default charset is US_ASCII. + */ +class StrictLineReader implements Closeable { + private static final byte CR = (byte) '\r'; + private static final byte LF = (byte) '\n'; + + private final InputStream in; + private final Charset charset; + + /* + * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end + * and the data in the range [pos, end) is buffered for reading. At end of input, if there is + * an unterminated line, we set end == -1, otherwise end == pos. If the underlying + * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + */ + private byte[] buf; + private int pos; + private int end; + + /** + * Constructs a new {@code LineReader} with the specified charset and the default capacity. + * + * @param in the {@code InputStream} to read data from. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if the specified charset is not supported. + */ + public StrictLineReader(InputStream in, Charset charset) { + this(in, 8192, charset); + } + + /** + * Constructs a new {@code LineReader} with the specified capacity and charset. + * + * @param in the {@code InputStream} to read data from. + * @param capacity the capacity of the buffer. + * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are + * supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if {@code capacity} is negative or zero + * or the specified charset is not supported. + */ + public StrictLineReader(InputStream in, int capacity, Charset charset) { + if (in == null || charset == null) { + throw new NullPointerException(); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity <= 0"); + } + if (!(charset.equals(Util.US_ASCII))) { + throw new IllegalArgumentException("Unsupported encoding"); + } + + this.in = in; + this.charset = charset; + buf = new byte[capacity]; + } + + /** + * Closes the reader by closing the underlying {@code InputStream} and + * marking this reader as closed. + * + * @throws IOException for errors when closing the underlying {@code InputStream}. + */ + public void close() throws IOException { + synchronized (in) { + if (buf != null) { + buf = null; + in.close(); + } + } + } + + /** + * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, + * this end of line marker is not included in the result. + * + * @return the next line from the input. + * @throws IOException for underlying {@code InputStream} errors. + * @throws EOFException for the end of source stream. + */ + public String readLine() throws IOException { + synchronized (in) { + if (buf == null) { + throw new IOException("LineReader is closed"); + } + + // Read more data if we are at the end of the buffered data. + // Though it's an error to read after an exception, we will let {@code fillBuf()} + // throw again if that happens; thus we need to handle end == -1 as well as end == pos. + if (pos >= end) { + fillBuf(); + } + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; + String res = new String(buf, pos, lineEnd - pos, charset.name()); + pos = i + 1; + return res; + } + } + + // Let's anticipate up to 80 characters on top of those already read. + ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { + @Override + public String toString() { + int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; + try { + return new String(buf, 0, length, charset.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // Since we control the charset this will never happen. + } + } + }; + + while (true) { + out.write(buf, pos, end - pos); + // Mark unterminated line in case fillBuf throws EOFException or IOException. + end = -1; + fillBuf(); + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + if (i != pos) { + out.write(buf, pos, i - pos); + } + pos = i + 1; + return out.toString(); + } + } + } + } + } + + public boolean hasUnterminatedLine() { + return end == -1; + } + + /** + * Reads new input data into the buffer. Call only with pos == end or end == -1, + * depending on the desired outcome if the function throws. + */ + private void fillBuf() throws IOException { + int result = in.read(buf, 0, buf.length); + if (result == -1) { + throw new EOFException(); + } + pos = 0; + end = result; + } +} + diff --git a/core/src/main/java/com/example/bumptech/glide/disklrucache/Util.java b/core/src/main/java/com/example/bumptech/glide/disklrucache/Util.java new file mode 100755 index 0000000..589ffdd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/disklrucache/Util.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2010 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.example.bumptech.glide.disklrucache; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.Charset; + +/** Junk drawer of utility methods. */ +final class Util { + static final Charset US_ASCII = Charset.forName("US-ASCII"); + static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ + static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("not a readable directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + static void closeQuietly(/*Auto*/Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifdecoder/BuildConfig.java b/core/src/main/java/com/example/bumptech/glide/gifdecoder/BuildConfig.java new file mode 100755 index 0000000..5d1cb51 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifdecoder/BuildConfig.java @@ -0,0 +1,13 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.example.bumptech.glide.gifdecoder; + +public final class BuildConfig { + public static final boolean DEBUG = false; + public static final String APPLICATION_ID = "com.bumptech.glide.gifdecoder"; + public static final String BUILD_TYPE = "release"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = -1; + public static final String VERSION_NAME = ""; +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifDecoder.java b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifDecoder.java new file mode 100755 index 0000000..718009e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifDecoder.java @@ -0,0 +1,710 @@ +package com.example.bumptech.glide.gifdecoder; + + +/** + * Copyright (c) 2013 Xcellent Creations, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Reads frame data from a GIF image source and decodes it into individual frames + * for animation purposes. Image data can be read from either and InputStream source + * or a byte[]. + * + * This class is optimized for running animations with the frames, there + * are no methods to get individual frame images, only to decode the next frame in the + * animation sequence. Instead, it lowers its memory footprint by only housing the minimum + * data necessary to decode the next frame in the animation sequence. + * + * The animation must be manually moved forward using {@link #advance()} before requesting the next + * frame. This method must also be called before you request the first frame or an error will + * occur. + * + * Implementation adapted from sample code published in Lyons. (2004). Java for Programmers, + * republished under the MIT Open Source License + */ +public class GifDecoder { + private static final String TAG = GifDecoder.class.getSimpleName(); + + /** + * File read status: No errors. + */ + public static final int STATUS_OK = 0; + /** + * File read status: Error decoding file (may be partially decoded). + */ + public static final int STATUS_FORMAT_ERROR = 1; + /** + * File read status: Unable to open source. + */ + public static final int STATUS_OPEN_ERROR = 2; + /** + * Unable to fully decode the current frame. + */ + public static final int STATUS_PARTIAL_DECODE = 3; + /** + * max decoder pixel stack size. + */ + private static final int MAX_STACK_SIZE = 4096; + + /** + * GIF Disposal Method meaning take no action. + */ + private static final int DISPOSAL_UNSPECIFIED = 0; + /** + * GIF Disposal Method meaning leave canvas from previous frame. + */ + private static final int DISPOSAL_NONE = 1; + /** + * GIF Disposal Method meaning clear canvas to background color. + */ + private static final int DISPOSAL_BACKGROUND = 2; + /** + * GIF Disposal Method meaning clear canvas to frame before last. + */ + private static final int DISPOSAL_PREVIOUS = 3; + + private static final int NULL_CODE = -1; + + private static final int INITIAL_FRAME_POINTER = -1; + + // We can't tell if a gif has transparency to decode a partial frame on top of a previous frame, or if the final + // frame will actually have transparent pixels, so we must always use a format that supports transparency. We can't + // use ARGB_4444 because of framework issues drawing onto ARGB_4444 Bitmaps using Canvas. + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + + // Global File Header values and parsing flags. + // Active color table. + private int[] act; + + // Raw GIF data from input source. + private ByteBuffer rawData; + + // Raw data read working array. + private final byte[] block = new byte[256]; + + private GifHeaderParser parser; + + // LZW decoder working arrays. + private short[] prefix; + private byte[] suffix; + private byte[] pixelStack; + private byte[] mainPixels; + private int[] mainScratch; + + private int framePointer; + private byte[] data; + private GifHeader header; + private BitmapProvider bitmapProvider; + private Bitmap previousImage; + private boolean savePrevious; + private int status; + private double delayRatio = 1; + + /** + * An interface that can be used to provide reused {@link Bitmap}s to avoid GCs from constantly + * allocating {@link Bitmap}s for every frame. + */ + public interface BitmapProvider { + /** + * Returns an {@link Bitmap} with exactly the given dimensions and config, or null if no such {@link Bitmap} + * could be obtained. + * + * @param width The width in pixels of the desired {@link Bitmap}. + * @param height The height in pixels of the desired {@link Bitmap}. + * @param config The {@link Bitmap.Config} of the desired {@link Bitmap}. + */ + public Bitmap obtain(int width, int height, Bitmap.Config config); + + /** + * Releases the given Bitmap back to the pool. + */ + public void release(Bitmap bitmap); + } + + public GifDecoder(BitmapProvider provider) { + this.bitmapProvider = provider; + header = new GifHeader(); + + } + + public int getWidth() { + return header.width; + } + + public int getHeight() { + return header.height; + } + + public byte[] getData() { + return data; + } + + /** + * Returns the current status of the decoder. + * + *

+ * Status will update per frame to allow the caller to tell whether or not the current frame was decoded + * successfully and/or completely. Format and open failures persist across frames. + *

+ */ + public int getStatus() { + return status; + } + + /** + * Move the animation frame counter forward. + */ + public void advance() { + framePointer = (framePointer + 1) % header.frameCount; + } + + /** + * Gets display duration for specified frame. + * + * @param n int index of frame. + * @return delay in milliseconds. + */ + public int getDelay(int n) { + int delay = -1; + if ((n >= 0) && (n < header.frameCount)) { + delay = header.frames.get(n).delay; + } + return delay; + } + + /** + * Gets display duration for the upcoming frame in ms. + */ + public int getNextDelay() { + if (header.frameCount <= 0 || framePointer < 0) { + return -1; + } + + return (int) (getDelay(framePointer) * delayRatio); + } + + public void setDelayRatio(double delayRatio) { + this.delayRatio = delayRatio; + } + + /** + * Gets the number of frames read from file. + * + * @return frame count. + */ + public int getFrameCount() { + return header.frameCount; + } + + /** + * Gets the current index of the animation frame, or -1 if animation hasn't not yet started. + * + * @return frame index. + */ + public int getCurrentFrameIndex() { + return framePointer; + } + + public void resetFrameIndex() { + framePointer = -1; + } + + /** + * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitely. + * + * @return iteration count if one was specified, else 1. + */ + public int getLoopCount() { + return header.loopCount; + } + + /** + * Get the next frame in the animation sequence. + * + * @return Bitmap representation of frame. + */ + public synchronized Bitmap getNextFrame() { + if (header.frameCount <= 0 || framePointer < 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "unable to decode frame, frameCount=" + header.frameCount + " framePointer=" + framePointer); + } + status = STATUS_FORMAT_ERROR; + } + if (status == STATUS_FORMAT_ERROR || status == STATUS_OPEN_ERROR) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unable to decode frame, status=" + status); + } + return null; + } + status = STATUS_OK; + + GifFrame currentFrame = header.frames.get(framePointer); + GifFrame previousFrame = null; + int previousIndex = framePointer - 1; + if (previousIndex >= 0) { + previousFrame = header.frames.get(previousIndex); + } + + // Set the appropriate color table. + if (currentFrame.lct == null) { + act = header.gct; + } else { + act = currentFrame.lct; + if (header.bgIndex == currentFrame.transIndex) { + header.bgColor = 0; + } + } + + int save = 0; + if (currentFrame.transparency) { + save = act[currentFrame.transIndex]; + // Set transparent color if specified. + act[currentFrame.transIndex] = 0; + } + if (act == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "No Valid Color Table"); + } + // No color table defined. + status = STATUS_FORMAT_ERROR; + return null; + } + + // Transfer pixel data to image. + Bitmap result = setPixels(currentFrame, previousFrame); + + // Reset the transparent pixel in the color table + if (currentFrame.transparency) { + act[currentFrame.transIndex] = save; + } + + return result; + } + + /** + * Reads GIF image from stream. + * + * @param is containing GIF file. + * @return read status code (0 = no errors). + */ + public int read(InputStream is, int contentLength) { + if (is != null) { + try { + int capacity = (contentLength > 0) ? (contentLength + 4096) : 16384; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity); + int nRead; + byte[] data = new byte[16384]; + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + + read(buffer.toByteArray()); + } catch (IOException e) { + Log.w(TAG, "Error reading data from stream", e); + } + } else { + status = STATUS_OPEN_ERROR; + } + + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + Log.w(TAG, "Error closing stream", e); + } + + return status; + } + + public void clear() { + header = null; + data = null; + mainPixels = null; + mainScratch = null; + if (previousImage != null) { + bitmapProvider.release(previousImage); + } + previousImage = null; + rawData = null; + } + + public void setData(GifHeader header, byte[] data) { + this.header = header; + this.data = data; + this.status = STATUS_OK; + framePointer = INITIAL_FRAME_POINTER; + // Initialize the raw data buffer. + rawData = ByteBuffer.wrap(data); + rawData.rewind(); + rawData.order(ByteOrder.LITTLE_ENDIAN); + + + // No point in specially saving an old frame if we're never going to use it. + savePrevious = false; + for (GifFrame frame : header.frames) { + if (frame.dispose == DISPOSAL_PREVIOUS) { + savePrevious = true; + break; + } + } + + // Now that we know the size, init scratch arrays. + mainPixels = new byte[header.width * header.height]; + mainScratch = new int[header.width * header.height]; + } + + private GifHeaderParser getHeaderParser() { + if (parser == null) { + parser = new GifHeaderParser(); + } + return parser; + } + + /** + * Reads GIF image from byte array. + * + * @param data containing GIF file. + * @return read status code (0 = no errors). + */ + public int read(byte[] data) { + this.data = data; + this.header = getHeaderParser().setData(data).parseHeader(); + if (data != null) { + // Initialize the raw data buffer. + rawData = ByteBuffer.wrap(data); + rawData.rewind(); + rawData.order(ByteOrder.LITTLE_ENDIAN); + + // Now that we know the size, init scratch arrays. + mainPixels = new byte[header.width * header.height]; + mainScratch = new int[header.width * header.height]; + + // No point in specially saving an old frame if we're never going to use it. + savePrevious = false; + for (GifFrame frame : header.frames) { + if (frame.dispose == DISPOSAL_PREVIOUS) { + savePrevious = true; + break; + } + } + } + + return status; + } + + /** + * Creates new frame image from current data (and previous frames as specified by their disposition codes). + */ + private Bitmap setPixels(GifFrame currentFrame, GifFrame previousFrame) { + + int width = header.width; + int height = header.height; + + // Final location of blended pixels. + final int[] dest = mainScratch; + + // fill in starting image contents based on last image's dispose code + if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) { + // We don't need to do anything for DISPOSAL_NONE, if it has the correct pixels so will our mainScratch + // and therefore so will our dest array. + if (previousFrame.dispose == DISPOSAL_BACKGROUND) { + // Start with a canvas filled with the background color + int c = 0; + if (!currentFrame.transparency) { + c = header.bgColor; + } + Arrays.fill(dest, c); + } else if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) { + // Start with the previous frame + previousImage.getPixels(dest, 0, width, 0, 0, width, height); + } + } + + // Decode pixels for this frame into the global pixels[] scratch. + decodeBitmapData(currentFrame); + + // Copy each source line to the appropriate place in the destination. + int pass = 1; + int inc = 8; + int iline = 0; + for (int i = 0; i < currentFrame.ih; i++) { + int line = i; + if (currentFrame.interlace) { + if (iline >= currentFrame.ih) { + pass++; + switch (pass) { + case 2: + iline = 4; + break; + case 3: + iline = 2; + inc = 4; + break; + case 4: + iline = 1; + inc = 2; + break; + default: + break; + } + } + line = iline; + iline += inc; + } + line += currentFrame.iy; + if (line < header.height) { + int k = line * header.width; + // Start of line in dest. + int dx = k + currentFrame.ix; + // End of dest line. + int dlim = dx + currentFrame.iw; + if ((k + header.width) < dlim) { + // Past dest edge. + dlim = k + header.width; + } + // Start of line in source. + int sx = i * currentFrame.iw; + while (dx < dlim) { + // Map color and insert in destination. + int index = ((int) mainPixels[sx++]) & 0xff; + int c = act[index]; + if (c != 0) { + dest[dx] = c; + } + dx++; + } + } + } + + // Copy pixels into previous image + if (savePrevious && (currentFrame.dispose == DISPOSAL_UNSPECIFIED + || currentFrame.dispose == DISPOSAL_NONE)) { + if (previousImage == null) { + previousImage = getNextBitmap(); + } + previousImage.setPixels(dest, 0, width, 0, 0, width, height); + } + + // Set pixels for current image. + Bitmap result = getNextBitmap(); + result.setPixels(dest, 0, width, 0, 0, width, height); + return result; + } + + /** + * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick. + */ + private void decodeBitmapData(GifFrame frame) { + if (frame != null) { + // Jump to the frame start position. + rawData.position(frame.bufferFrameStart); + } + + int npix = (frame == null) ? header.width * header.height : frame.iw * frame.ih; + int available, clear, codeMask, codeSize, endOfInformation, inCode, oldCode, bits, code, count, i, datum, + dataSize, first, top, bi, pi; + + if (mainPixels == null || mainPixels.length < npix) { + // Allocate new pixel array. + mainPixels = new byte[npix]; + } + if (prefix == null) { + prefix = new short[MAX_STACK_SIZE]; + } + if (suffix == null) { + suffix = new byte[MAX_STACK_SIZE]; + } + if (pixelStack == null) { + pixelStack = new byte[MAX_STACK_SIZE + 1]; + } + + // Initialize GIF data stream decoder. + dataSize = read(); + clear = 1 << dataSize; + endOfInformation = clear + 1; + available = clear + 2; + oldCode = NULL_CODE; + codeSize = dataSize + 1; + codeMask = (1 << codeSize) - 1; + for (code = 0; code < clear; code++) { + // XXX ArrayIndexOutOfBoundsException. + prefix[code] = 0; + suffix[code] = (byte) code; + } + + // Decode GIF pixel stream. + datum = bits = count = first = top = pi = bi = 0; + for (i = 0; i < npix; ) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + count = readBlock(); + if (count <= 0) { + status = STATUS_PARTIAL_DECODE; + break; + } + bi = 0; + } + + datum += (((int) block[bi]) & 0xff) << bits; + bits += 8; + bi++; + count--; + + while (bits >= codeSize) { + // Get the next code. + code = datum & codeMask; + datum >>= codeSize; + bits -= codeSize; + + // Interpret the code. + if (code == clear) { + // Reset decoder. + codeSize = dataSize + 1; + codeMask = (1 << codeSize) - 1; + available = clear + 2; + oldCode = NULL_CODE; + continue; + } + + if (code > available) { + status = STATUS_PARTIAL_DECODE; + break; + } + + if (code == endOfInformation) { + break; + } + + if (oldCode == NULL_CODE) { + pixelStack[top++] = suffix[code]; + oldCode = code; + first = code; + continue; + } + inCode = code; + if (code >= available) { + pixelStack[top++] = (byte) first; + code = oldCode; + } + while (code >= clear) { + pixelStack[top++] = suffix[code]; + code = prefix[code]; + } + first = ((int) suffix[code]) & 0xff; + pixelStack[top++] = (byte) first; + + // Add a new string to the string table. + if (available < MAX_STACK_SIZE) { + prefix[available] = (short) oldCode; + suffix[available] = (byte) first; + available++; + if (((available & codeMask) == 0) && (available < MAX_STACK_SIZE)) { + codeSize++; + codeMask += available; + } + } + oldCode = inCode; + + while (top > 0) { + // Pop a pixel off the pixel stack. + top--; + mainPixels[pi++] = pixelStack[top]; + i++; + } + } + } + + // Clear missing pixels. + for (i = pi; i < npix; i++) { + mainPixels[i] = 0; + } + } + + /** + * Reads a single byte from the input stream. + */ + private int read() { + int curByte = 0; + try { + curByte = rawData.get() & 0xFF; + } catch (Exception e) { + status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer". + */ + private int readBlock() { + int blockSize = read(); + int n = 0; + if (blockSize > 0) { + try { + int count; + while (n < blockSize) { + count = blockSize - n; + rawData.get(block, n, count); + + n += count; + } + } catch (Exception e) { + Log.w(TAG, "Error Reading Block", e); + status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + private Bitmap getNextBitmap() { + Bitmap result = bitmapProvider.obtain(header.width, header.height, BITMAP_CONFIG); + if (result == null) { + result = Bitmap.createBitmap(header.width, header.height, BITMAP_CONFIG); + } + setAlpha(result); + return result; + } + + @TargetApi(12) + private static void setAlpha(Bitmap bitmap) { + if (Build.VERSION.SDK_INT >= 12) { + bitmap.setHasAlpha(true); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifFrame.java b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifFrame.java new file mode 100755 index 0000000..c1182df --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifFrame.java @@ -0,0 +1,22 @@ +package com.example.bumptech.glide.gifdecoder; + +/** + * Inner model class housing metadata for each frame. + */ +class GifFrame { + int ix, iy, iw, ih; + /** Control Flag. */ + boolean interlace; + /** Control Flag. */ + boolean transparency; + /** Disposal Method. */ + int dispose; + /** Transparency Index. */ + int transIndex; + /** Delay, in ms, to next frame. */ + int delay; + /** Index in the raw buffer where we need to start reading to decode. */ + int bufferFrameStart; + /** Local Color Table. */ + int[] lct; +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeader.java b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeader.java new file mode 100755 index 0000000..3ff249f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeader.java @@ -0,0 +1,57 @@ +package com.example.bumptech.glide.gifdecoder; + +import java.util.ArrayList; +import java.util.List; + +/** + * A header object containing the number of frames in an animated GIF image as well as basic metadata like width and + * height that can be used to decode each individual frame of the GIF. Can be shared by one or more + * {@link GifDecoder}s to play the same animated GIF in multiple views. + */ +public class GifHeader { + + int[] gct = null; + int status = GifDecoder.STATUS_OK; + int frameCount = 0; + + GifFrame currentFrame; + List frames = new ArrayList(); + // Logical screen size. + // Full image width. + int width; + // Full image height. + int height; + + // 1 : global color table flag. + boolean gctFlag; + // 2-4 : color resolution. + // 5 : gct sort flag. + // 6-8 : gct size. + int gctSize; + // Background color index. + int bgIndex; + // Pixel aspect ratio. + int pixelAspect; + //TODO: this is set both during reading the header and while decoding frames... + int bgColor; + int loopCount; + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public int getNumFrames() { + return frameCount; + } + + /** + * Global status code of GIF data parsing. + */ + public int getStatus() { + return status; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeaderParser.java b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeaderParser.java new file mode 100755 index 0000000..36581bb --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifdecoder/GifHeaderParser.java @@ -0,0 +1,375 @@ +package com.example.bumptech.glide.gifdecoder; + +import android.util.Log; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import static com.example.bumptech.glide.gifdecoder.GifDecoder.STATUS_FORMAT_ERROR; + + +/** + * A class responsible for creating {@link GifHeader}s from data representing animated + * gifs. + */ +public class GifHeaderParser { + public static final String TAG = "GifHeaderParser"; + + // The minimum frame delay in hundredths of a second. + static final int MIN_FRAME_DELAY = 3; + // The default frame delay in hundredths of a second for GIFs with frame delays less than the minimum. + static final int DEFAULT_FRAME_DELAY = 10; + + private static final int MAX_BLOCK_SIZE = 256; + // Raw data read working array. + private final byte[] block = new byte[MAX_BLOCK_SIZE]; + + private ByteBuffer rawData; + private GifHeader header; + private int blockSize = 0; + + public GifHeaderParser setData(byte[] data) { + reset(); + if (data != null) { + rawData = ByteBuffer.wrap(data); + rawData.rewind(); + rawData.order(ByteOrder.LITTLE_ENDIAN); + } else { + rawData = null; + header.status = GifDecoder.STATUS_OPEN_ERROR; + } + return this; + } + + public void clear() { + rawData = null; + header = null; + } + + private void reset() { + rawData = null; + Arrays.fill(block, (byte) 0); + header = new GifHeader(); + blockSize = 0; + } + + public GifHeader parseHeader() { + if (rawData == null) { + throw new IllegalStateException("You must call setData() before parseHeader()"); + } + if (err()) { + return header; + } + + readHeader(); + if (!err()) { + readContents(); + if (header.frameCount < 0) { + header.status = STATUS_FORMAT_ERROR; + } + } + + return header; + } + + /** + * Main file parser. Reads GIF content blocks. + */ + private void readContents() { + // Read GIF file content blocks. + boolean done = false; + while (!(done || err())) { + int code = read(); + switch (code) { + // Image separator. + case 0x2C: + // The graphics control extension is optional, but will always come first if it exists. If one did + // exist, there will be a non-null current frame which we should use. However if one did not exist, + // the current frame will be null and we must create it here. See issue #134. + if (header.currentFrame == null) { + header.currentFrame = new GifFrame(); + } + readBitmap(); + break; + // Extension. + case 0x21: + code = read(); + switch (code) { + // Graphics control extension. + case 0xf9: + // Start a new frame. + header.currentFrame = new GifFrame(); + readGraphicControlExt(); + break; + // Application extension. + case 0xff: + readBlock(); + String app = ""; + for (int i = 0; i < 11; i++) { + app += (char) block[i]; + } + if (app.equals("NETSCAPE2.0")) { + readNetscapeExt(); + } else { + // Don't care. + skip(); + } + break; + // Comment extension. + case 0xfe: + skip(); + break; + // Plain text extension. + case 0x01: + skip(); + break; + // Uninteresting extension. + default: + skip(); + } + break; + // Terminator. + case 0x3b: + done = true; + break; + // Bad byte, but keep going and see what happens break; + case 0x00: + default: + header.status = STATUS_FORMAT_ERROR; + } + } + } + + /** + * Reads Graphics Control Extension values. + */ + private void readGraphicControlExt() { + // Block size. + read(); + // Packed fields. + int packed = read(); + // Disposal method. + header.currentFrame.dispose = (packed & 0x1c) >> 2; + if (header.currentFrame.dispose == 0) { + // Elect to keep old image if discretionary. + header.currentFrame.dispose = 1; + } + header.currentFrame.transparency = (packed & 1) != 0; + // Delay in milliseconds. + int delayInHundredthsOfASecond = readShort(); + // TODO: consider allowing -1 to indicate show forever. + if (delayInHundredthsOfASecond < MIN_FRAME_DELAY) { + delayInHundredthsOfASecond = DEFAULT_FRAME_DELAY; + } + header.currentFrame.delay = delayInHundredthsOfASecond * 10; + // Transparent color index + header.currentFrame.transIndex = read(); + // Block terminator + read(); + } + + /** + * Reads next frame image. + */ + private void readBitmap() { + // (sub)image position & size. + header.currentFrame.ix = readShort(); + header.currentFrame.iy = readShort(); + header.currentFrame.iw = readShort(); + header.currentFrame.ih = readShort(); + + int packed = read(); + // 1 - local color table flag interlace + boolean lctFlag = (packed & 0x80) != 0; + int lctSize = (int) Math.pow(2, (packed & 0x07) + 1); + // 3 - sort flag + // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color + // table size + header.currentFrame.interlace = (packed & 0x40) != 0; + if (lctFlag) { + // Read table. + header.currentFrame.lct = readColorTable(lctSize); + } else { + // No local color table. + header.currentFrame.lct = null; + } + + // Save this as the decoding position pointer. + header.currentFrame.bufferFrameStart = rawData.position(); + + // False decode pixel data to advance buffer. + skipImageData(); + + if (err()) { + return; + } + + header.frameCount++; + // Add image to frame. + header.frames.add(header.currentFrame); + } + /** + * Reads Netscape extension to obtain iteration count. + */ + private void readNetscapeExt() { + do { + readBlock(); + if (block[0] == 1) { + // Loop count sub-block. + int b1 = ((int) block[1]) & 0xff; + int b2 = ((int) block[2]) & 0xff; + header.loopCount = (b2 << 8) | b1; + } + } while ((blockSize > 0) && !err()); + } + + + /** + * Reads GIF file header information. + */ + private void readHeader() { + String id = ""; + for (int i = 0; i < 6; i++) { + id += (char) read(); + } + if (!id.startsWith("GIF")) { + header.status = STATUS_FORMAT_ERROR; + return; + } + readLSD(); + if (header.gctFlag && !err()) { + header.gct = readColorTable(header.gctSize); + header.bgColor = header.gct[header.bgIndex]; + } + } + /** + * Reads Logical Screen Descriptor. + */ + private void readLSD() { + // Logical screen size. + header.width = readShort(); + header.height = readShort(); + // Packed fields + int packed = read(); + // 1 : global color table flag. + header.gctFlag = (packed & 0x80) != 0; + // 2-4 : color resolution. + // 5 : gct sort flag. + // 6-8 : gct size. + header.gctSize = 2 << (packed & 7); + // Background color index. + header.bgIndex = read(); + // Pixel aspect ratio + header.pixelAspect = read(); + } + + /** + * Reads color table as 256 RGB integer values. + * + * @param ncolors int number of colors to read. + * @return int array containing 256 colors (packed ARGB with full alpha). + */ + private int[] readColorTable(int ncolors) { + int nbytes = 3 * ncolors; + int[] tab = null; + byte[] c = new byte[nbytes]; + + try { + rawData.get(c); + + // TODO: what bounds checks are we avoiding if we know the number of colors? + // Max size to avoid bounds checks. + tab = new int[MAX_BLOCK_SIZE]; + int i = 0; + int j = 0; + while (i < ncolors) { + int r = ((int) c[j++]) & 0xff; + int g = ((int) c[j++]) & 0xff; + int b = ((int) c[j++]) & 0xff; + tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b; + } + } catch (BufferUnderflowException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Format Error Reading Color Table", e); + } + header.status = STATUS_FORMAT_ERROR; + } + + return tab; + } + + /** + * Skips LZW image data for a single frame to advance buffer. + */ + private void skipImageData() { + // lzwMinCodeSize + read(); + // data sub-blocks + skip(); + } + + /** + * Skips variable length blocks up to and including next zero length block. + */ + private void skip() { + int blockSize; + do { + blockSize = read(); + rawData.position(rawData.position() + blockSize); + } while (blockSize > 0); + } + + /** + * Reads next variable length block from input. + * + * @return number of bytes stored in "buffer" + */ + private int readBlock() { + blockSize = read(); + int n = 0; + if (blockSize > 0) { + int count = 0; + try { + while (n < blockSize) { + count = blockSize - n; + rawData.get(block, n, count); + + n += count; + } + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Error Reading Block n: " + n + " count: " + count + " blockSize: " + blockSize, e); + } + header.status = STATUS_FORMAT_ERROR; + } + } + return n; + } + + /** + * Reads a single byte from the input stream. + */ + private int read() { + int curByte = 0; + try { + curByte = rawData.get() & 0xFF; + } catch (Exception e) { + header.status = STATUS_FORMAT_ERROR; + } + return curByte; + } + + /** + * Reads next 16-bit value, LSB first. + */ + private int readShort() { + // Read 16-bit value. + return rawData.getShort(); + } + + private boolean err() { + return header.status != GifDecoder.STATUS_OK; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifencoder/AnimatedGifEncoder.java b/core/src/main/java/com/example/bumptech/glide/gifencoder/AnimatedGifEncoder.java new file mode 100755 index 0000000..440c1a6 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifencoder/AnimatedGifEncoder.java @@ -0,0 +1,531 @@ +package com.example.bumptech.glide.gifencoder; + + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Class AnimatedGifEncoder - Encodes a GIF file consisting of one or more + * frames. + * + *
+ *  Example:
+ *     AnimatedGifEncoder e = new AnimatedGifEncoder();
+ *     e.start(outputFileName);
+ *     e.setDelay(1000);   // 1 frame per sec
+ *     e.addFrame(image1);
+ *     e.addFrame(image2);
+ *     e.finish();
+ * 
+ * + * No copyright asserted on the source code of this class. May be used for any + * purpose, however, refer to the Unisys LZW patent for restrictions on use of + * the associated LZWEncoder class. Please forward any corrections to + * kweiner@fmsware.com. + * + * @author Kevin Weiner, FM Software + * @version 1.03 November 2003 + * + */ + +public class AnimatedGifEncoder { + private static final String TAG = "AnimatedGifEncoder"; + + // The minimum % of an images pixels that must be transparent for us to set a transparent index automatically. + private static final double MIN_TRANSPARENT_PERCENTAGE = 4d; + + private int width; // image size + + private int height; + + private Integer transparent = null; // transparent color if given + + private int transIndex; // transparent index in color table + + private int repeat = -1; // no repeat + + private int delay = 0; // frame delay (hundredths) + + private boolean started = false; // ready to output frames + + private OutputStream out; + + private Bitmap image; // current frame + + private byte[] pixels; // BGR byte array from frame + + private byte[] indexedPixels; // converted frame indexed to palette + + private int colorDepth; // number of bit planes + + private byte[] colorTab; // RGB palette + + private boolean[] usedEntry = new boolean[256]; // active palette entries + + private int palSize = 7; // color table size (bits-1) + + private int dispose = -1; // disposal code (-1 = use default) + + private boolean closeStream = false; // close stream when finished + + private boolean firstFrame = true; + + private boolean sizeSet = false; // if false, get size from first frame + + private int sample = 10; // default sample interval for quantizer + + private boolean hasTransparentPixels; + + /** + * Sets the delay time between each frame, or changes it for subsequent frames + * (applies to last frame added). + * + * @param ms + * int delay time in milliseconds + */ + public void setDelay(int ms) { + delay = Math.round(ms / 10.0f); + } + + /** + * Sets the GIF frame disposal code for the last added frame and any + * subsequent frames. Default is 0 if no transparent color has been set, + * otherwise 2. + * + * @param code + * int disposal code. + */ + public void setDispose(int code) { + if (code >= 0) { + dispose = code; + } + } + + /** + * Sets the number of times the set of GIF frames should be played. Default is + * 1; 0 means play indefinitely. Must be invoked before the first image is + * added. + * + * @param iter + * int number of iterations. + */ + public void setRepeat(int iter) { + if (iter >= 0) { + repeat = iter; + } + } + + /** + * Sets the transparent color for the last added frame and any subsequent + * frames. Since all colors are subject to modification in the quantization + * process, the color in the final palette for each frame closest to the given + * color becomes the transparent color for that frame. May be set to null to + * indicate no transparent color. + * + * @param color + * Color to be treated as transparent on display. + */ + public void setTransparent(int color) { + transparent = color; + } + + /** + * Adds next GIF frame. The frame is not written immediately, but is actually + * deferred until the next frame is received so that timing data can be + * inserted. Invoking finish() flushes all frames. If + * setSize was not invoked, the size of the first image is used + * for all subsequent frames. + * + * @param im + * BufferedImage containing frame to write. + * @return true if successful. + */ + public boolean addFrame(Bitmap im) { + if ((im == null) || !started) { + return false; + } + boolean ok = true; + try { + if (!sizeSet) { + // use first frame's size + setSize(im.getWidth(), im.getHeight()); + } + image = im; + getImagePixels(); // convert to correct format if necessary + analyzePixels(); // build color table & map pixels + if (firstFrame) { + writeLSD(); // logical screen descriptior + writePalette(); // global color table + if (repeat >= 0) { + // use NS app extension to indicate reps + writeNetscapeExt(); + } + } + writeGraphicCtrlExt(); // write graphic control extension + writeImageDesc(); // image descriptor + if (!firstFrame) { + writePalette(); // local color table + } + writePixels(); // encode and write pixel data + firstFrame = false; + } catch (IOException e) { + ok = false; + } + + return ok; + } + + /** + * Flushes any pending data and closes output file. If writing to an + * OutputStream, the stream is not closed. + */ + public boolean finish() { + if (!started) + return false; + boolean ok = true; + started = false; + try { + out.write(0x3b); // gif trailer + out.flush(); + if (closeStream) { + out.close(); + } + } catch (IOException e) { + ok = false; + } + + // reset for subsequent use + transIndex = 0; + out = null; + image = null; + pixels = null; + indexedPixels = null; + colorTab = null; + closeStream = false; + firstFrame = true; + + return ok; + } + + /** + * Sets frame rate in frames per second. Equivalent to + * setDelay(1000/fps). + * + * @param fps + * float frame rate (frames per second) + */ + public void setFrameRate(float fps) { + if (fps != 0f) { + delay = Math.round(100f / fps); + } + } + + /** + * Sets quality of color quantization (conversion of images to the maximum 256 + * colors allowed by the GIF specification). Lower values (minimum = 1) + * produce better colors, but slow processing significantly. 10 is the + * default, and produces good color mapping at reasonable speeds. Values + * greater than 20 do not yield significant improvements in speed. + * + * @param quality int greater than 0. + */ + public void setQuality(int quality) { + if (quality < 1) + quality = 1; + sample = quality; + } + + /** + * Sets the GIF frame size. The default size is the size of the first frame + * added if this method is not invoked. + * + * @param w + * int frame width. + * @param h + * int frame width. + */ + public void setSize(int w, int h) { + if (started && !firstFrame) + return; + width = w; + height = h; + if (width < 1) + width = 320; + if (height < 1) + height = 240; + sizeSet = true; + } + + /** + * Initiates GIF file creation on the given stream. The stream is not closed + * automatically. + * + * @param os + * OutputStream on which GIF images are written. + * @return false if initial write failed. + */ + public boolean start(OutputStream os) { + if (os == null) + return false; + boolean ok = true; + closeStream = false; + out = os; + try { + writeString("GIF89a"); // header + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Initiates writing of a GIF file with the specified name. + * + * @param file + * String containing output file name. + * @return false if open or initial write failed. + */ + public boolean start(String file) { + boolean ok = true; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + ok = start(out); + closeStream = true; + } catch (IOException e) { + ok = false; + } + return started = ok; + } + + /** + * Analyzes image colors and creates color map. + */ + private void analyzePixels() { + int len = pixels.length; + int nPix = len / 3; + indexedPixels = new byte[nPix]; + NeuQuant nq = new NeuQuant(pixels, len, sample); + // initialize quantizer + colorTab = nq.process(); // create reduced palette + // convert map from BGR to RGB + for (int i = 0; i < colorTab.length; i += 3) { + byte temp = colorTab[i]; + colorTab[i] = colorTab[i + 2]; + colorTab[i + 2] = temp; + usedEntry[i / 3] = false; + } + // map image pixels to new palette + int k = 0; + for (int i = 0; i < nPix; i++) { + int index = nq.map(pixels[k++] & 0xff, pixels[k++] & 0xff, pixels[k++] & 0xff); + usedEntry[index] = true; + indexedPixels[i] = (byte) index; + } + pixels = null; + colorDepth = 8; + palSize = 7; + // get closest match to transparent color if specified + if (transparent != null) { + transIndex = findClosest(transparent); + } else if (hasTransparentPixels) { + transIndex = findClosest(Color.TRANSPARENT); + } + } + + /** + * Returns index of palette color closest to c + * + */ + private int findClosest(int color) { + if (colorTab == null) + return -1; + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + int minpos = 0; + int dmin = 256 * 256 * 256; + int len = colorTab.length; + for (int i = 0; i < len;) { + int dr = r - (colorTab[i++] & 0xff); + int dg = g - (colorTab[i++] & 0xff); + int db = b - (colorTab[i] & 0xff); + int d = dr * dr + dg * dg + db * db; + int index = i / 3; + if (usedEntry[index] && (d < dmin)) { + dmin = d; + minpos = index; + } + i++; + } + return minpos; + } + + /** + * Extracts image pixels into byte array "pixels" + */ + private void getImagePixels() { + int w = image.getWidth(); + int h = image.getHeight(); + + if ((w != width) || (h != height)) { + // create new image with right size/format + Bitmap temp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(temp); + canvas.drawBitmap(temp, 0, 0, null); + image = temp; + } + int[] pixelsInt = new int[w * h]; + image.getPixels(pixelsInt, 0, w, 0, 0, w, h); + + // The algorithm requires 3 bytes per pixel as RGB. + pixels = new byte[pixelsInt.length * 3]; + + int pixelsIndex = 0; + hasTransparentPixels = false; + int totalTransparentPixels = 0; + for (final int pixel : pixelsInt) { + if (pixel == Color.TRANSPARENT) { + totalTransparentPixels++; + } + pixels[pixelsIndex++] = (byte) (pixel & 0xFF); + pixels[pixelsIndex++] = (byte) ((pixel >> 8) & 0xFF); + pixels[pixelsIndex++] = (byte) ((pixel >> 16) & 0xFF); + } + + double transparentPercentage = 100 * totalTransparentPixels / (double) pixelsInt.length; + // Assume images with greater where more than n% of the pixels are transparent actually have transparency. + // See issue #214. + hasTransparentPixels = transparentPercentage > MIN_TRANSPARENT_PERCENTAGE; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "got pixels for frame with " + transparentPercentage + "% transparent pixels"); + } + } + + /** + * Writes Graphic Control Extension + */ + private void writeGraphicCtrlExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xf9); // GCE label + out.write(4); // data block size + int transp, disp; + if (transparent == null && !hasTransparentPixels) { + transp = 0; + disp = 0; // dispose = no action + } else { + transp = 1; + disp = 2; // force clear if using transparent color + } + if (dispose >= 0) { + disp = dispose & 7; // user override + } + disp <<= 2; + + // packed fields + out.write(0 | // 1:3 reserved + disp | // 4:6 disposal + 0 | // 7 user input - 0 = none + transp); // 8 transparency flag + + writeShort(delay); // delay x 1/100 sec + out.write(transIndex); // transparent color index + out.write(0); // block terminator + } + + /** + * Writes Image Descriptor + */ + private void writeImageDesc() throws IOException { + out.write(0x2c); // image separator + writeShort(0); // image position x,y = 0,0 + writeShort(0); + writeShort(width); // image size + writeShort(height); + // packed fields + if (firstFrame) { + // no LCT - GCT is used for first (or only) frame + out.write(0); + } else { + // specify normal LCT + out.write(0x80 | // 1 local color table 1=yes + 0 | // 2 interlace - 0=no + 0 | // 3 sorted - 0=no + 0 | // 4-5 reserved + palSize); // 6-8 size of color table + } + } + + /** + * Writes Logical Screen Descriptor + */ + private void writeLSD() throws IOException { + // logical screen size + writeShort(width); + writeShort(height); + // packed fields + out.write((0x80 | // 1 : global color table flag = 1 (gct used) + 0x70 | // 2-4 : color resolution = 7 + 0x00 | // 5 : gct sort flag = 0 + palSize)); // 6-8 : gct size + + out.write(0); // background color index + out.write(0); // pixel aspect ratio - assume 1:1 + } + + /** + * Writes Netscape application extension to define repeat count. + */ + private void writeNetscapeExt() throws IOException { + out.write(0x21); // extension introducer + out.write(0xff); // app extension label + out.write(11); // block size + writeString("NETSCAPE" + "2.0"); // app id + auth code + out.write(3); // sub-block size + out.write(1); // loop sub-block id + writeShort(repeat); // loop count (extra iterations, 0=repeat forever) + out.write(0); // block terminator + } + + /** + * Writes color table + */ + private void writePalette() throws IOException { + out.write(colorTab, 0, colorTab.length); + int n = (3 * 256) - colorTab.length; + for (int i = 0; i < n; i++) { + out.write(0); + } + } + + /** + * Encodes and writes pixel data + */ + private void writePixels() throws IOException { + LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth); + encoder.encode(out); + } + + /** + * Write 16-bit value to output stream, LSB first + */ + private void writeShort(int value) throws IOException { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + } + + /** + * Writes string to output stream + */ + private void writeString(String s) throws IOException { + for (int i = 0; i < s.length(); i++) { + out.write((byte) s.charAt(i)); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifencoder/BuildConfig.java b/core/src/main/java/com/example/bumptech/glide/gifencoder/BuildConfig.java new file mode 100755 index 0000000..0997937 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifencoder/BuildConfig.java @@ -0,0 +1,13 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.example.bumptech.glide.gifencoder; + +public final class BuildConfig { + public static final boolean DEBUG = false; + public static final String APPLICATION_ID = "com.bumptech.glide.gifencoder"; + public static final String BUILD_TYPE = "release"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = -1; + public static final String VERSION_NAME = ""; +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifencoder/LZWEncoder.java b/core/src/main/java/com/example/bumptech/glide/gifencoder/LZWEncoder.java new file mode 100755 index 0000000..9c773cd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifencoder/LZWEncoder.java @@ -0,0 +1,297 @@ +package com.example.bumptech.glide.gifencoder; + +import java.io.IOException; +import java.io.OutputStream; + +// ============================================================================== +// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. +// K Weiner 12/00 + +class LZWEncoder { + + private static final int EOF = -1; + + private int imgW, imgH; + + private byte[] pixAry; + + private int initCodeSize; + + private int remaining; + + private int curPixel; + + // GIFCOMPR.C - GIF Image compression routines + // + // Lempel-Ziv compression based on 'compress'. GIF modifications by + // David Rowley (mgardi@watdcsu.waterloo.edu) + + // General DEFINEs + + static final int BITS = 12; + + static final int HSIZE = 5003; // 80% occupancy + + // GIF Image compression - modified 'compress' + // + // Based on: compress.c - File compression ala IEEE Computer, June 1984. + // + // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) + // Jim McKie (decvax!mcvax!jim) + // Steve Davies (decvax!vax135!petsd!peora!srd) + // Ken Turkowski (decvax!decwrl!turtlevax!ken) + // James A. Woods (decvax!ihnp4!ames!jaw) + // Joe Orost (decvax!vax135!petsd!joe) + + int n_bits; // number of bits/code + + int maxbits = BITS; // user settable max # bits/code + + int maxcode; // maximum code, given n_bits + + int maxmaxcode = 1 << BITS; // should NEVER generate this code + + int[] htab = new int[HSIZE]; + + int[] codetab = new int[HSIZE]; + + int hsize = HSIZE; // for dynamic table sizing + + int free_ent = 0; // first unused entry + + // block compression parameters -- after all codes are used up, + // and compression rate changes, start over. + boolean clear_flg = false; + + // Algorithm: use open addressing double hashing (no chaining) on the + // prefix code / next character combination. We do a variant of Knuth's + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime + // secondary probe. Here, the modular division first probe is gives way + // to a faster exclusive-or manipulation. Also do block compression with + // an adaptive reset, whereby the code table is cleared when the compression + // ratio decreases, but after the table fills. The variable-length output + // codes are re-sized at this point, and a special CLEAR code is generated + // for the decompressor. Late addition: construct the table according to + // file size for noticeable speed improvement on small files. Please direct + // questions about this implementation to ames!jaw. + + int g_init_bits; + + int ClearCode; + + int EOFCode; + + // output + // + // Output the given code. + // Inputs: + // code: A n_bits-bit integer. If == -1, then EOF. This assumes + // that n_bits =< wordsize - 1. + // Outputs: + // Outputs code to the file. + // Assumptions: + // Chars are 8 bits long. + // Algorithm: + // Maintain a BITS character long buffer (so that 8 codes will + // fit in it exactly). Use the VAX insv instruction to insert each + // code in turn. When the buffer fills up empty it and start over. + + int cur_accum = 0; + + int cur_bits = 0; + + int masks[] = {0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, + 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF}; + + // Number of characters so far in this 'packet' + int a_count; + + // Define the storage for the packet accumulator + byte[] accum = new byte[256]; + + // ---------------------------------------------------------------------------- + LZWEncoder(int width, int height, byte[] pixels, int color_depth) { + imgW = width; + imgH = height; + pixAry = pixels; + initCodeSize = Math.max(2, color_depth); + } + + // Add a character to the end of the current packet, and if it is 254 + // characters, flush the packet to disk. + void char_out(byte c, OutputStream outs) throws IOException { + accum[a_count++] = c; + if (a_count >= 254) + flush_char(outs); + } + + // Clear out the hash table + + // table clear for block compress + void cl_block(OutputStream outs) throws IOException { + cl_hash(hsize); + free_ent = ClearCode + 2; + clear_flg = true; + + output(ClearCode, outs); + } + + // reset code table + void cl_hash(int hsize) { + for (int i = 0; i < hsize; ++i) + htab[i] = -1; + } + + void compress(int init_bits, OutputStream outs) throws IOException { + int fcode; + int i /* = 0 */; + int c; + int ent; + int disp; + int hsize_reg; + int hshift; + + // Set up the globals: g_init_bits - initial number of bits + g_init_bits = init_bits; + + // Set up the necessary values + clear_flg = false; + n_bits = g_init_bits; + maxcode = MAXCODE(n_bits); + + ClearCode = 1 << (init_bits - 1); + EOFCode = ClearCode + 1; + free_ent = ClearCode + 2; + + a_count = 0; // clear packet + + ent = nextPixel(); + + hshift = 0; + for (fcode = hsize; fcode < 65536; fcode *= 2) + ++hshift; + hshift = 8 - hshift; // set hash code range bound + + hsize_reg = hsize; + cl_hash(hsize_reg); // clear hash table + + output(ClearCode, outs); + + outer_loop: + while ((c = nextPixel()) != EOF) { + fcode = (c << maxbits) + ent; + i = (c << hshift) ^ ent; // xor hashing + + if (htab[i] == fcode) { + ent = codetab[i]; + continue; + } else if (htab[i] >= 0) // non-empty slot + { + disp = hsize_reg - i; // secondary hash (after G. Knott) + if (i == 0) + disp = 1; + do { + if ((i -= disp) < 0) + i += hsize_reg; + + if (htab[i] == fcode) { + ent = codetab[i]; + continue outer_loop; + } + } while (htab[i] >= 0); + } + output(ent, outs); + ent = c; + if (free_ent < maxmaxcode) { + codetab[i] = free_ent++; // code -> hashtable + htab[i] = fcode; + } else + cl_block(outs); + } + // Put out the final code. + output(ent, outs); + output(EOFCode, outs); + } + + // ---------------------------------------------------------------------------- + void encode(OutputStream os) throws IOException { + os.write(initCodeSize); // write "initial code size" byte + + remaining = imgW * imgH; // reset navigation variables + curPixel = 0; + + compress(initCodeSize + 1, os); // compress and write the pixel data + + os.write(0); // write block terminator + } + + // Flush the packet to disk, and reset the accumulator + void flush_char(OutputStream outs) throws IOException { + if (a_count > 0) { + outs.write(a_count); + outs.write(accum, 0, a_count); + a_count = 0; + } + } + + final int MAXCODE(int n_bits) { + return (1 << n_bits) - 1; + } + + // ---------------------------------------------------------------------------- + // Return the next pixel from the image + // ---------------------------------------------------------------------------- + private int nextPixel() { + if (remaining == 0) + return EOF; + + --remaining; + + byte pix = pixAry[curPixel++]; + + return pix & 0xff; + } + + void output(int code, OutputStream outs) throws IOException { + cur_accum &= masks[cur_bits]; + + if (cur_bits > 0) + cur_accum |= (code << cur_bits); + else + cur_accum = code; + + cur_bits += n_bits; + + while (cur_bits >= 8) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + // If the next entry is going to be too big for the code size, + // then increase it, if possible. + if (free_ent > maxcode || clear_flg) { + if (clear_flg) { + maxcode = MAXCODE(n_bits = g_init_bits); + clear_flg = false; + } else { + ++n_bits; + if (n_bits == maxbits) + maxcode = maxmaxcode; + else + maxcode = MAXCODE(n_bits); + } + } + + if (code == EOFCode) { + // At EOF, write the rest of the buffer. + while (cur_bits > 0) { + char_out((byte) (cur_accum & 0xff), outs); + cur_accum >>= 8; + cur_bits -= 8; + } + + flush_char(outs); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/gifencoder/NeuQuant.java b/core/src/main/java/com/example/bumptech/glide/gifencoder/NeuQuant.java new file mode 100755 index 0000000..0d39bd2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/gifencoder/NeuQuant.java @@ -0,0 +1,506 @@ +package com.example.bumptech.glide.gifencoder; + +/* + * NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See + * "Kohonen neural networks for optimal colour quantization" in "Network: + * Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of + * the algorithm. + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal in + * this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons who + * receive copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + */ + +// Ported to Java 12/00 K Weiner +class NeuQuant { + + protected static final int netsize = 256; /* number of colours used */ + + /* four primes near 500 - assume no image has a length so large */ + /* that it is divisible by all four primes */ + protected static final int prime1 = 499; + + protected static final int prime2 = 491; + + protected static final int prime3 = 487; + + protected static final int prime4 = 503; + + protected static final int minpicturebytes = (3 * prime4); + + /* minimum size for input image */ + + /* + * Program Skeleton ---------------- [select samplefac in range 1..30] [read + * image from input file] pic = (unsigned char*) malloc(3*width*height); + * initnet(pic,3*width*height,samplefac); learn(); unbiasnet(); [write output + * image header, using writecolourmap(f)] inxbuild(); write output image using + * inxsearch(b,g,r) + */ + + /* + * Network Definitions ------------------- + */ + + protected static final int maxnetpos = (netsize - 1); + + protected static final int netbiasshift = 4; /* bias for colour values */ + + protected static final int ncycles = 100; /* no. of learning cycles */ + + /* defs for freq and bias */ + protected static final int intbiasshift = 16; /* bias for fractions */ + + protected static final int intbias = (((int) 1) << intbiasshift); + + protected static final int gammashift = 10; /* gamma = 1024 */ + + protected static final int gamma = (((int) 1) << gammashift); + + protected static final int betashift = 10; + + protected static final int beta = (intbias >> betashift); /* beta = 1/1024 */ + + protected static final int betagamma = (intbias << (gammashift - betashift)); + + /* defs for decreasing radius factor */ + protected static final int initrad = (netsize >> 3); /* + * for 256 cols, radius + * starts + */ + + protected static final int radiusbiasshift = 6; /* at 32.0 biased by 6 bits */ + + protected static final int radiusbias = (((int) 1) << radiusbiasshift); + + protected static final int initradius = (initrad * radiusbias); /* + * and + * decreases + * by a + */ + + protected static final int radiusdec = 30; /* factor of 1/30 each cycle */ + + /* defs for decreasing alpha factor */ + protected static final int alphabiasshift = 10; /* alpha starts at 1.0 */ + + protected static final int initalpha = (((int) 1) << alphabiasshift); + + protected int alphadec; /* biased by 10 bits */ + + /* radbias and alpharadbias used for radpower calculation */ + protected static final int radbiasshift = 8; + + protected static final int radbias = (((int) 1) << radbiasshift); + + protected static final int alpharadbshift = (alphabiasshift + radbiasshift); + + protected static final int alpharadbias = (((int) 1) << alpharadbshift); + + /* + * Types and Global Variables -------------------------- + */ + + protected byte[] thepicture; /* the input image itself */ + + protected int lengthcount; /* lengthcount = H*W*3 */ + + protected int samplefac; /* sampling factor 1..30 */ + + // typedef int pixel[4]; /* BGRc */ + protected int[][] network; /* the network itself - [netsize][4] */ + + protected int[] netindex = new int[256]; + + /* for network lookup - really 256 */ + + protected int[] bias = new int[netsize]; + + /* bias and freq arrays for learning */ + protected int[] freq = new int[netsize]; + + protected int[] radpower = new int[initrad]; + + /* radpower for precomputation */ + + /* + * Initialise network in range (0,0,0) to (255,255,255) and set parameters + * ----------------------------------------------------------------------- + */ + public NeuQuant(byte[] thepic, int len, int sample) { + + int i; + int[] p; + + thepicture = thepic; + lengthcount = len; + samplefac = sample; + + network = new int[netsize][]; + for (i = 0; i < netsize; i++) { + network[i] = new int[4]; + p = network[i]; + p[0] = p[1] = p[2] = (i << (netbiasshift + 8)) / netsize; + freq[i] = intbias / netsize; /* 1/netsize */ + bias[i] = 0; + } + } + + public byte[] colorMap() { + byte[] map = new byte[3 * netsize]; + int[] index = new int[netsize]; + for (int i = 0; i < netsize; i++) + index[network[i][3]] = i; + int k = 0; + for (int i = 0; i < netsize; i++) { + int j = index[i]; + map[k++] = (byte) (network[j][0]); + map[k++] = (byte) (network[j][1]); + map[k++] = (byte) (network[j][2]); + } + return map; + } + + /* + * Insertion sort of network and building of netindex[0..255] (to do after + * unbias) + * ------------------------------------------------------------------------------- + */ + public void inxbuild() { + + int i, j, smallpos, smallval; + int[] p; + int[] q; + int previouscol, startpos; + + previouscol = 0; + startpos = 0; + for (i = 0; i < netsize; i++) { + p = network[i]; + smallpos = i; + smallval = p[1]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j = i + 1; j < netsize; j++) { + q = network[j]; + if (q[1] < smallval) { /* index on g */ + smallpos = j; + smallval = q[1]; /* index on g */ + } + } + q = network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + netindex[previouscol] = (startpos + i) >> 1; + for (j = previouscol + 1; j < smallval; j++) + netindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + netindex[previouscol] = (startpos + maxnetpos) >> 1; + for (j = previouscol + 1; j < 256; j++) + netindex[j] = maxnetpos; /* really 256 */ + } + + /* + * Main Learning Loop ------------------ + */ + public void learn() { + + int i, j, b, g, r; + int radius, rad, alpha, step, delta, samplepixels; + byte[] p; + int pix, lim; + + if (lengthcount < minpicturebytes) + samplefac = 1; + alphadec = 30 + ((samplefac - 1) / 3); + p = thepicture; + pix = 0; + lim = lengthcount; + samplepixels = lengthcount / (3 * samplefac); + delta = samplepixels / ncycles; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (i = 0; i < rad; i++) + radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); + + // fprintf(stderr,"beginning 1D learning: initial radius=%d\n", rad); + + if (lengthcount < minpicturebytes) + step = 3; + else if ((lengthcount % prime1) != 0) + step = 3 * prime1; + else { + if ((lengthcount % prime2) != 0) + step = 3 * prime2; + else { + if ((lengthcount % prime3) != 0) + step = 3 * prime3; + else + step = 3 * prime4; + } + } + + i = 0; + while (i < samplepixels) { + b = (p[pix + 0] & 0xff) << netbiasshift; + g = (p[pix + 1] & 0xff) << netbiasshift; + r = (p[pix + 2] & 0xff) << netbiasshift; + j = contest(b, g, r); + + altersingle(alpha, j, b, g, r); + if (rad != 0) + alterneigh(rad, j, b, g, r); /* alter neighbours */ + + pix += step; + if (pix >= lim) + pix -= lengthcount; + + i++; + if (delta == 0) + delta = 1; + if (i % delta == 0) { + alpha -= alpha / alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) + rad = 0; + for (j = 0; j < rad; j++) + radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); + } + } + // fprintf(stderr,"finished 1D learning: final alpha=%f + // !\n",((float)alpha)/initalpha); + } + + /* + * Search for BGR values 0..255 (after net is unbiased) and return colour + * index + * ---------------------------------------------------------------------------- + */ + public int map(int b, int g, int r) { + + int i, j, dist, a, bestd; + int[] p; + int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = -1; + i = netindex[g]; /* index on g */ + j = i - 1; /* start at netindex[g] and work outwards */ + + while ((i < netsize) || (j >= 0)) { + if (i < netsize) { + p = network[i]; + dist = p[1] - g; /* inx key */ + if (dist >= bestd) + i = netsize; /* stop iter */ + else { + i++; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + if (j >= 0) { + p = network[j]; + dist = g - p[1]; /* inx key - reverse dif */ + if (dist >= bestd) + j = -1; /* stop iter */ + else { + j--; + if (dist < 0) + dist = -dist; + a = p[0] - b; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + a = p[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + best = p[3]; + } + } + } + } + } + return (best); + } + + public byte[] process() { + learn(); + unbiasnet(); + inxbuild(); + return colorMap(); + } + + /* + * Unbias network to give byte values 0..255 and record position i to prepare + * for sort + * ----------------------------------------------------------------------------------- + */ + public void unbiasnet() { + + int i, j; + + for (i = 0; i < netsize; i++) { + network[i][0] >>= netbiasshift; + network[i][1] >>= netbiasshift; + network[i][2] >>= netbiasshift; + network[i][3] = i; /* record colour no */ + } + } + + /* + * Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in + * radpower[|i-j|] + * --------------------------------------------------------------------------------- + */ + protected void alterneigh(int rad, int i, int b, int g, int r) { + + int j, k, lo, hi, a, m; + int[] p; + + lo = i - rad; + if (lo < -1) + lo = -1; + hi = i + rad; + if (hi > netsize) + hi = netsize; + + j = i + 1; + k = i - 1; + m = 1; + while ((j < hi) || (k > lo)) { + a = radpower[m++]; + if (j < hi) { + p = network[j++]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } // prevents 1.3 miscompilation + } + if (k > lo) { + p = network[k--]; + try { + p[0] -= (a * (p[0] - b)) / alpharadbias; + p[1] -= (a * (p[1] - g)) / alpharadbias; + p[2] -= (a * (p[2] - r)) / alpharadbias; + } catch (Exception e) { + } + } + } + } + + /* + * Move neuron i towards biased (b,g,r) by factor alpha + * ---------------------------------------------------- + */ + protected void altersingle(int alpha, int i, int b, int g, int r) { + + /* alter hit neuron */ + int[] n = network[i]; + n[0] -= (alpha * (n[0] - b)) / initalpha; + n[1] -= (alpha * (n[1] - g)) / initalpha; + n[2] -= (alpha * (n[2] - r)) / initalpha; + } + + /* + * Search for biased BGR values ---------------------------- + */ + protected int contest(int b, int g, int r) { + + /* finds closest neuron (min dist) and updates freq */ + /* finds best neuron (min dist-bias) and returns position */ + /* for frequently chosen neurons, freq[i] is high and bias[i] is negative */ + /* bias[i] = gamma*((1/netsize)-freq[i]) */ + + int i, dist, a, biasdist, betafreq; + int bestpos, bestbiaspos, bestd, bestbiasd; + int[] n; + + bestd = ~(((int) 1) << 31); + bestbiasd = bestd; + bestpos = -1; + bestbiaspos = bestpos; + + for (i = 0; i < netsize; i++) { + n = network[i]; + dist = n[0] - b; + if (dist < 0) + dist = -dist; + a = n[1] - g; + if (a < 0) + a = -a; + dist += a; + a = n[2] - r; + if (a < 0) + a = -a; + dist += a; + if (dist < bestd) { + bestd = dist; + bestpos = i; + } + biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); + if (biasdist < bestbiasd) { + bestbiasd = biasdist; + bestbiaspos = i; + } + betafreq = (freq[i] >> betashift); + freq[i] -= betafreq; + bias[i] += (betafreq << gammashift); + } + freq[bestpos] += beta; + bias[bestpos] -= betagamma; + return (bestbiaspos); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/DecodeFormat.java b/core/src/main/java/com/example/bumptech/glide/load/DecodeFormat.java new file mode 100755 index 0000000..427f189 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/DecodeFormat.java @@ -0,0 +1,49 @@ +package com.example.bumptech.glide.load; + +/** + * Options for setting the value of {@link android.graphics.Bitmap#getConfig()} for {@link android.graphics.Bitmap}s + * returned by a {@link com.bumptech.glide.load.resource.bitmap.BitmapDecoder}. + * + *

+ * Note - In some cases it may not be possible to obey the requested setting, not all + * {@link com.bumptech.glide.load.resource.bitmap.BitmapDecoder}s support setting formats and certain images may + * not be able to be loaded as certain configurations. Therefore this class represents a preference rather than a + * requirement. + *

+ */ +public enum DecodeFormat { + /** + * All bitmaps returned by the {@link com.bumptech.glide.load.resource.bitmap.BitmapDecoder} should return + * {@link android.graphics.Bitmap.Config#ARGB_8888} for {@link android.graphics.Bitmap#getConfig()}. + * + * @deprecated Use the equivalent but less misleadingly named {@link #PREFER_ARGB_8888}. Scheduled to be removed + * in Glide 4.0 + */ + @Deprecated + ALWAYS_ARGB_8888, + + /** + * Bitmaps decoded from most image formats (other than GIFs with hidden configs), will be decoded with the + * ARGB_8888 config. + * + *

+ * {@link android.graphics.BitmapFactory} does not allow us to guarantee that all returned Bitmaps will + * be of a requested config without resorting to expensive copying. As a result, this is a preference only. + * Most GIFs, for example, will still produce {@link android.graphics.Bitmap}s with null + * {@link android.graphics.Bitmap.Config}s. + *

+ */ + PREFER_ARGB_8888, + + /** + * Bitmaps decoded from image formats that support and/or use alpha (some types of PNGs, GIFs etc) should + * return {@link android.graphics.Bitmap.Config#ARGB_8888} for {@link android.graphics.Bitmap#getConfig()}. Bitmaps + * decoded from formats that don't support or use alpha should return + * {@link android.graphics.Bitmap.Config#RGB_565} for {@link android.graphics.Bitmap#getConfig()}. + * + */ + PREFER_RGB_565; + + /** The default value for DecodeFormat. */ + public static final DecodeFormat DEFAULT = PREFER_RGB_565; +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/Encoder.java b/core/src/main/java/com/example/bumptech/glide/load/Encoder.java new file mode 100755 index 0000000..8c1858b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/Encoder.java @@ -0,0 +1,31 @@ +package com.example.bumptech.glide.load; + +import java.io.OutputStream; + +/** + * An interface for writing data to some persistent data store (i.e. a local File cache). + * + * @param The type of the data that will be written. + */ +public interface Encoder { + + /** + * Writes the given data to the given output stream and returns True if the write completed successfully and + * should be committed. + * + * @param data The data to write. + * @param os The OutputStream to write the data to. + */ + boolean encode(T data, OutputStream os); + + /** + * Returns an ID identifying any transformation this encoder may apply to the given data that will be mixed in to + * the cache key. + * + *

+ * If the encoder does not transform the data in a way that significantly affects the cached result (ie performs + * no unusual compression or downsampling) an empty string is an appropriate id. + *

+ */ + String getId(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/Key.java b/core/src/main/java/com/example/bumptech/glide/load/Key.java new file mode 100755 index 0000000..dd33718 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/Key.java @@ -0,0 +1,31 @@ +package com.example.bumptech.glide.load; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; + +/** + * An interface that uniquely identifies some set of data. Implementations must implement {@link Object#equals(Object)} + * and {@link Object#hashCode()}. Implementations are generally expected to add all uniquely identifying information + * used in in {@link Object#equals(Object)}} and {@link Object#hashCode()}} to the given + * {@link MessageDigest} in {@link #updateDiskCacheKey(MessageDigest)}}, although this + * requirement is not as strict for partial cache key signatures. + */ +public interface Key { + String STRING_CHARSET_NAME = "UTF-8"; + + /** + * Adds all uniquely identifying information to the given digest. + * + *

+ * Note - Using {@link MessageDigest#reset()} inside of this method will result in undefined + * behavior. + *

+ */ + void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException; + + @Override + boolean equals(Object o); + + @Override + int hashCode(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/MultiTransformation.java b/core/src/main/java/com/example/bumptech/glide/load/MultiTransformation.java new file mode 100755 index 0000000..899f861 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/MultiTransformation.java @@ -0,0 +1,58 @@ +package com.example.bumptech.glide.load; + + +import com.example.bumptech.glide.load.engine.Resource; + +import java.util.Arrays; +import java.util.Collection; + +/** + * A transformation that applies one or more transformations in iteration order to a resource. + * + * @param The type of {@link Resource} that will be transformed. + */ +public class MultiTransformation implements Transformation { + private final Collection> transformations; + private String id; + + @SafeVarargs + public MultiTransformation(Transformation... transformations) { + if (transformations.length < 1) { + throw new IllegalArgumentException("MultiTransformation must contain at least one Transformation"); + } + this.transformations = Arrays.asList(transformations); + } + + public MultiTransformation(Collection> transformationList) { + if (transformationList.size() < 1) { + throw new IllegalArgumentException("MultiTransformation must contain at least one Transformation"); + } + this.transformations = transformationList; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + Resource previous = resource; + + for (Transformation transformation : transformations) { + Resource transformed = transformation.transform(previous, outWidth, outHeight); + if (previous != null && !previous.equals(resource) && !previous.equals(transformed)) { + previous.recycle(); + } + previous = transformed; + } + return previous; + } + + @Override + public String getId() { + if (id == null) { + StringBuilder sb = new StringBuilder(); + for (Transformation transformation : transformations) { + sb.append(transformation.getId()); + } + id = sb.toString(); + } + return id; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/ResourceDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/ResourceDecoder.java new file mode 100755 index 0000000..9730e6d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/ResourceDecoder.java @@ -0,0 +1,50 @@ +package com.example.bumptech.glide.load; + + +import com.example.bumptech.glide.load.engine.Resource; + +import java.io.IOException; + +/** + * An interface for decoding resources. + * + * @param The type the resource will be decoded from (File, InputStream etc). + * @param The type of the decoded resource (Bitmap, Drawable etc). + */ +public interface ResourceDecoder { + + /** + * Returns a decoded resource from the given data or null if no resource could be decoded. + *

+ * The {@code source} is managed by the caller, there's no need to close it. + * The returned {@link Resource} will be {@link Resource#recycle() released} when the engine sees fit. + *

+ *

+ * Note - The {@code width} and {@code height} arguments are hints only, + * there is no requirement that the decoded resource exactly match the given dimensions. + * A typical use case would be to use the target dimensions to determine + * how much to downsample Bitmaps by to avoid overly large allocations. + *

+ * + * @param source The data the resource should be decoded from. + * @param width The ideal width in pixels of the decoded resource, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource + * width. + * @param height The ideal height in pixels of the decoded resource, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original resource + * height. + * @throws IOException + */ + Resource decode(T source, int width, int height) throws IOException; + + /** + * Returns an ID identifying any transformation this decoder may apply to the given data that will be mixed in to + * the cache key. + * + *

+ * If the decoder does not transform the data in a way that significantly affects the cached + * result (ie performs no downsampling) an empty string is an appropriate id. + *

+ */ + String getId(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/ResourceEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/ResourceEncoder.java new file mode 100755 index 0000000..0931e32 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/ResourceEncoder.java @@ -0,0 +1,13 @@ +package com.example.bumptech.glide.load; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * An interface for writing data from a resource to some persistent data store (i.e. a local File cache). + * + * @param The type of the data contained by the resource. + */ +public interface ResourceEncoder extends Encoder> { + // specializing the generic arguments +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/Transformation.java b/core/src/main/java/com/example/bumptech/glide/load/Transformation.java new file mode 100755 index 0000000..aea17e9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/Transformation.java @@ -0,0 +1,47 @@ +package com.example.bumptech.glide.load; + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A class for performing an arbitrary transformation on a resource. + * + * @param The type of the resource being transformed. + */ +public interface Transformation { + + /** + * Transforms the given resource and returns the transformed resource. + * + *

+ * Note - If the original resource object is not returned, the original resource will be recycled and it's + * internal resources may be reused. This means it is not safe to rely on the original resource or any internal + * state of the original resource in any new resource that is created. Usually this shouldn't occur, but if + * absolutely necessary either the original resource object can be returned with modified internal state, or + * the data in the original resource can be copied into the transformed resource. + *

+ * + * @param resource The resource to transform. + * @param outWidth The width of the view or target the resource will be displayed in, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original + * resource width. + * @param outHeight The height of the view or target the resource will be displayed in, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate the original + * resource height. + * @return The transformed resource. + */ + Resource transform(Resource resource, int outWidth, int outHeight); + + /** + * A method to get a unique identifier for this particular transformation that can be used as part of a cache key. + * The fully qualified class name for this class is appropriate if written out, but getClass().getName() is not + * because the name may be changed by proguard. + * + *

+ * If this transformation does not affect the data that will be stored in cache, returning an empty string here + * is acceptable. + *

+ * + * @return A string that uniquely identifies this transformation. + */ + String getId(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/AssetPathFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/AssetPathFetcher.java new file mode 100755 index 0000000..e40afa1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/AssetPathFetcher.java @@ -0,0 +1,74 @@ +package com.example.bumptech.glide.load.data; + +import android.content.res.AssetManager; +import android.util.Log; + + +import com.example.bumptech.glide.Priority; + +import java.io.IOException; + +/** + * An abstract class for obtaining data for an asset path using an {@link AssetManager}. + * + * @param The type of data obtained from the asset path (InputStream, FileDescriptor etc). + */ +public abstract class AssetPathFetcher implements DataFetcher { + private static final String TAG = "AssetUriFetcher"; + private final String assetPath; + private final AssetManager assetManager; + private T data; + + public AssetPathFetcher(AssetManager assetManager, String assetPath) { + this.assetManager = assetManager; + this.assetPath = assetPath; + } + + @Override + public T loadData(Priority priority) throws Exception { + data = loadResource(assetManager, assetPath); + return data; + } + + @Override + public void cleanup() { + if (data == null) { + return; + } + try { + close(data); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Failed to close data", e); + } + } + + } + + @Override + public String getId() { + return assetPath; + } + + @Override + public void cancel() { + // Do nothing. + } + + /** + * Opens the given asset path with the given {@link AssetManager} and returns the conrete data + * type returned by the AssetManager. + * + * @param assetManager An AssetManager to use to open the given path. + * @param path A string path pointing to a resource in assets to open. + */ + protected abstract T loadResource(AssetManager assetManager, String path) throws IOException; + + /** + * Closes the concrete data type if necessary. + * + * @param data The data to close. + * @throws IOException + */ + protected abstract void close(T data) throws IOException; +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/ByteArrayFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/ByteArrayFetcher.java new file mode 100755 index 0000000..258706c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/ByteArrayFetcher.java @@ -0,0 +1,41 @@ +package com.example.bumptech.glide.load.data; + + +import com.example.bumptech.glide.Priority; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * A simple resource fetcher to convert byte arrays into input stream. Requires an id to be passed in to identify the + * data in the byte array because there is no cheap/simple way to obtain a useful id from the data itself. + */ +public class ByteArrayFetcher implements DataFetcher { + private final byte[] bytes; + private final String id; + + public ByteArrayFetcher(byte[] bytes, String id) { + this.bytes = bytes; + this.id = id; + } + + @Override + public InputStream loadData(Priority priority) { + return new ByteArrayInputStream(bytes); + } + + @Override + public void cleanup() { + // Do nothing. It's safe to leave a ByteArrayInputStream open. + } + + @Override + public String getId() { + return id; + } + + @Override + public void cancel() { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/DataFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/DataFetcher.java new file mode 100755 index 0000000..b0cca7b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/DataFetcher.java @@ -0,0 +1,77 @@ +package com.example.bumptech.glide.load.data; + + +import com.example.bumptech.glide.Priority; + +/** + * An interface for lazily retrieving data that can be used to load a resource. A new instance is created per + * resource load by {@link com.bumptech.glide.load.model.ModelLoader}. {@link #loadData(Priority)} may or may not be + * called for any given load depending on whether or not the corresponding resource is cached. Cancel also may or may + * not be called. If {@link #loadData(Priority)} is called, then so {@link #cleanup()} will be called. + * + * @param The type of data to be loaded (InputStream, byte[], File etc). + */ +public interface DataFetcher { + + /** + * Asynchronously fetch data from which a resource can be decoded. This will always be called on + * background thread so it is safe to perform long running tasks here. Any third party libraries called + * must be thread safe since this method will be called from a thread in a + * {@link java.util.concurrent.ExecutorService} that may have more than one background thread. + * + * This method will only be called when the corresponding resource is not in the cache. + * + *

+ * Note - this method will be run on a background thread so blocking I/O is safe. + *

+ * + * @param priority The priority with which the request should be completed. + * @see #cleanup() where the data retuned will be cleaned up + */ + T loadData(Priority priority) throws Exception; + + /** + * Cleanup or recycle any resources used by this data fetcher. This method will be called in a finally block + * after the data returned by {@link #loadData(Priority)} has been decoded by the + * {@link com.bumptech.glide.load.ResourceDecoder}. + * + *

+ * Note - this method will be run on a background thread so blocking I/O is safe. + *

+ * + */ + void cleanup(); + + /** + * Returns a string uniquely identifying the data that this fetcher will fetch including the specific size. + * + *

+ * A hash of the bytes of the data that will be fetched is the ideal id but since that is in many cases + * impractical, urls, file paths, and uris are normally sufficient. + *

+ * + *

+ * Note - this method will be run on the main thread so it should not perform blocking operations and should + * finish quickly. + *

+ */ + String getId(); + + /** + * A method that will be called when a load is no longer relevant and has been cancelled. This method does not need + * to guarantee that any in process loads do not finish. It also may be called before a load starts or after it + * finishes. + * + *

+ * The best way to use this method is to cancel any loads that have not yet started, but allow those that are in + * process to finish since its we typically will want to display the same resource in a different view in + * the near future. + *

+ * + *

+ * Note - this method will be run on the main thread so it should not perform blocking operations and should + * finish quickly. + *

+ */ + void cancel(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/ExifOrientationStream.java b/core/src/main/java/com/example/bumptech/glide/load/data/ExifOrientationStream.java new file mode 100755 index 0000000..f70a11e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/ExifOrientationStream.java @@ -0,0 +1,133 @@ +package com.example.bumptech.glide.load.data; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Adds an exif segment with an orientation attribute to a wrapped {@link InputStream} containing + * image data. + * + *

This class assumes that the wrapped stream contains an image format that can contain + * exif information and performs no verification.

+ */ +public class ExifOrientationStream extends FilterInputStream { + /** Allow two bytes for the file format. */ + private static final int SEGMENT_START_POSITION = 2; + private static final byte[] EXIF_SEGMENT = new byte[] { + /** segment start id. */ + (byte) 0xFF, + /** segment type. */ + (byte) 0xE1, + /** segmentLength. */ + 0x00, + (byte) 0x1C, + /** exif identifier. */ + 0x45, + 0x78, + 0x69, + 0x66, + 0x00, + 0x00, + /** mototorola byte order (big endian). */ + (byte) 0x4D, + (byte) 0x4D, + /** filler? */ + 0x00, + 0x00, + /** first id offset. */ + 0x00, + 0x00, + 0x00, + 0x08, + /** tagCount. */ + 0x00, + 0x01, + /** exif tag type. */ + 0x01, + 0x12, + /** 2 byte format. */ + 0x00, + 0x02, + /** component count. */ + 0x00, + 0x00, + 0x00, + 0x01, + /** 2 byte orientation value, the first byte of which is always 0. */ + 0x00, + }; + private static final int SEGMENT_LENGTH = EXIF_SEGMENT.length; + private static final int ORIENTATION_POSITION = SEGMENT_LENGTH + SEGMENT_START_POSITION; + private final byte orientation; + private int position; + + public ExifOrientationStream(InputStream in, int orientation) { + super(in); + if (orientation < -1 || orientation > 8) { + throw new IllegalArgumentException("Cannot add invalid orientation: " + orientation); + } + this.orientation = (byte) orientation; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + final int result; + if (position < SEGMENT_START_POSITION || position > ORIENTATION_POSITION) { + result = super.read(); + } else if (position == ORIENTATION_POSITION) { + result = orientation; + } else { + result = EXIF_SEGMENT[position - SEGMENT_START_POSITION] & 0xFF; + } + if (result != -1) { + position++; + } + return result; + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + int read; + if (position > ORIENTATION_POSITION) { + read = super.read(buffer, byteOffset, byteCount); + } else if (position == ORIENTATION_POSITION) { + buffer[byteOffset] = orientation; + read = 1; + } else if (position < SEGMENT_START_POSITION) { + read = super.read(buffer, byteOffset, SEGMENT_START_POSITION - position); + } else { + read = Math.min(ORIENTATION_POSITION - position, byteCount); + System.arraycopy(EXIF_SEGMENT, position - SEGMENT_START_POSITION, buffer, byteOffset, + read); + } + if (read > 0) { + position += read; + } + return read; + } + + @Override + public long skip(long byteCount) throws IOException { + long skipped = super.skip(byteCount); + if (skipped > 0) { + position += skipped; + } + return skipped; + } + + @Override + public void reset() throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorAssetPathFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorAssetPathFetcher.java new file mode 100755 index 0000000..ada0a01 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorAssetPathFetcher.java @@ -0,0 +1,25 @@ +package com.example.bumptech.glide.load.data; + +import android.content.res.AssetManager; +import android.os.ParcelFileDescriptor; + +import java.io.IOException; + +/** + * Fetches an {@link ParcelFileDescriptor} for an asset path. + */ +public class FileDescriptorAssetPathFetcher extends AssetPathFetcher { + public FileDescriptorAssetPathFetcher(AssetManager assetManager, String assetPath) { + super(assetManager, assetPath); + } + + @Override + protected ParcelFileDescriptor loadResource(AssetManager assetManager, String path) throws IOException { + return assetManager.openFd(path).getParcelFileDescriptor(); + } + + @Override + protected void close(ParcelFileDescriptor data) throws IOException { + data.close(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorLocalUriFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorLocalUriFetcher.java new file mode 100755 index 0000000..eab08f4 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/FileDescriptorLocalUriFetcher.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.load.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Fetches an {@link ParcelFileDescriptor} for a local {@link Uri}. + */ +public class FileDescriptorLocalUriFetcher extends LocalUriFetcher { + public FileDescriptorLocalUriFetcher(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected ParcelFileDescriptor loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { + return contentResolver.openAssetFileDescriptor(uri, "r").getParcelFileDescriptor(); + } + + @Override + protected void close(ParcelFileDescriptor data) throws IOException { + data.close(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/HttpUrlFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/HttpUrlFetcher.java new file mode 100755 index 0000000..5ecc11a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/HttpUrlFetcher.java @@ -0,0 +1,145 @@ +package com.example.bumptech.glide.load.data; + +import android.text.TextUtils; +import android.util.Log; + + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.util.ContentLengthInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; + +/** + * A DataFetcher that retrieves an {@link InputStream} for a Url. + */ +public class HttpUrlFetcher implements DataFetcher { + private static final String TAG = "HttpUrlFetcher"; + private static final int MAXIMUM_REDIRECTS = 5; + private static final HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultHttpUrlConnectionFactory(); + + private final GlideUrl glideUrl; + private final HttpUrlConnectionFactory connectionFactory; + + private HttpURLConnection urlConnection; + private InputStream stream; + private volatile boolean isCancelled; + + public HttpUrlFetcher(GlideUrl glideUrl) { + this(glideUrl, DEFAULT_CONNECTION_FACTORY); + } + + // Visible for testing. + HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) { + this.glideUrl = glideUrl; + this.connectionFactory = connectionFactory; + } + + @Override + public InputStream loadData(Priority priority) throws Exception { + return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders()); + } + + private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map headers) + throws IOException { + if (redirects >= MAXIMUM_REDIRECTS) { + throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!"); + } else { + // Comparing the URLs using .equals performs additional network I/O and is generally broken. + // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html. + try { + if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) { + throw new IOException("In re-direct loop"); + } + } catch (URISyntaxException e) { + // Do nothing, this is best effort. + } + } + urlConnection = connectionFactory.build(url); + for (Map.Entry headerEntry : headers.entrySet()) { + urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue()); + } + urlConnection.setConnectTimeout(2500); + urlConnection.setReadTimeout(2500); + urlConnection.setUseCaches(false); + urlConnection.setDoInput(true); + + // Connect explicitly to avoid errors in decoders if connection fails. + urlConnection.connect(); + if (isCancelled) { + return null; + } + final int statusCode = urlConnection.getResponseCode(); + if (statusCode / 100 == 2) { + return getStreamForSuccessfulRequest(urlConnection); + } else if (statusCode / 100 == 3) { + String redirectUrlString = urlConnection.getHeaderField("Location"); + if (TextUtils.isEmpty(redirectUrlString)) { + throw new IOException("Received empty or null redirect url"); + } + URL redirectUrl = new URL(url, redirectUrlString); + return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers); + } else { + if (statusCode == -1) { + throw new IOException("Unable to retrieve response code from HttpUrlConnection."); + } + throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage()); + } + } + + private InputStream getStreamForSuccessfulRequest(HttpURLConnection urlConnection) + throws IOException { + if (TextUtils.isEmpty(urlConnection.getContentEncoding())) { + int contentLength = urlConnection.getContentLength(); + stream = ContentLengthInputStream.obtain(urlConnection.getInputStream(), contentLength); + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got non empty content encoding: " + urlConnection.getContentEncoding()); + } + stream = urlConnection.getInputStream(); + } + return stream; + } + + @Override + public void cleanup() { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + // Ignore + } + } + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + + @Override + public String getId() { + return glideUrl.getCacheKey(); + } + + @Override + public void cancel() { + // TODO: we should consider disconnecting the url connection here, but we can't do so directly because cancel is + // often called on the main thread. + isCancelled = true; + } + + interface HttpUrlConnectionFactory { + HttpURLConnection build(URL url) throws IOException; + } + + private static class DefaultHttpUrlConnectionFactory implements HttpUrlConnectionFactory { + @Override + public HttpURLConnection build(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/LocalUriFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/LocalUriFetcher.java new file mode 100755 index 0000000..ffab3bd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/LocalUriFetcher.java @@ -0,0 +1,94 @@ +package com.example.bumptech.glide.load.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + + +import com.example.bumptech.glide.Priority; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * A DataFetcher that uses an {@link ContentResolver} to load data from a {@link Uri} + * pointing to a local resource. + * + * @param The type of data that will obtained for the given uri (For example, {@link java.io.InputStream} or + * {@link android.os.ParcelFileDescriptor}. + */ +public abstract class LocalUriFetcher implements DataFetcher { + private static final String TAG = "LocalUriFetcher"; + private final Uri uri; + private final Context context; + private T data; + + /** + * Opens an input stream for a uri pointing to a local asset. Only certain uris are supported + * + * @see ContentResolver#openInputStream(Uri) + * + * @param context A context (this will be weakly referenced and the load will fail if the weak reference + * is cleared before {@link #loadData(Priority)}} is called. + * @param uri A Uri pointing to a local asset. This load will fail if the uri isn't openable by + * {@link ContentResolver#openInputStream(Uri)} + */ + public LocalUriFetcher(Context context, Uri uri) { + this.context = context.getApplicationContext(); + this.uri = uri; + } + + @Override + public final T loadData(Priority priority) throws Exception { + ContentResolver contentResolver = context.getContentResolver(); + data = loadResource(uri, contentResolver); + return data; + } + + @Override + public void cleanup() { + if (data != null) { + try { + close(data); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failed to close data", e); + } + } + + } + } + + @Override + public void cancel() { + // Do nothing. + } + + @Override + public String getId() { + return uri.toString(); + } + + + /** + * Returns a concrete data type from the given {@link Uri} using the given + * {@link ContentResolver}. + * + * @throws FileNotFoundException + */ + protected abstract T loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException; + + /** + * Closes the concrete data type if necessary. + * + *

+ * Note - We can't rely on the closeable interface because it was added after our min API level. See issue #157. + *

+ * + * @param data The data to close. + * @throws IOException + */ + protected abstract void close(T data) throws IOException; +} + diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/MediaStoreThumbFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/MediaStoreThumbFetcher.java new file mode 100755 index 0000000..f51481a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/MediaStoreThumbFetcher.java @@ -0,0 +1,262 @@ +package com.example.bumptech.glide.load.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A DataFetcher that retrieves an {@link InputStream} for a local Uri that may or may not be for a resource + * in the media store. If the local Uri is for a resource in the media store and the size requested is less than or + * equal to the media store thumbnail size, preferentially attempts to fetch data for the pre-generated media store + * thumbs using {@link MediaStore.Images.Thumbnails} and + * {@link MediaStore.Video.Thumbnails}. + */ +public class MediaStoreThumbFetcher implements DataFetcher { + private static final String TAG = "MediaStoreThumbFetcher"; + private static final int MINI_WIDTH = 512; + private static final int MINI_HEIGHT = 384; + private static final ThumbnailStreamOpenerFactory DEFAULT_FACTORY = new ThumbnailStreamOpenerFactory(); + + private final Context context; + private final Uri mediaStoreUri; + private final DataFetcher defaultFetcher; + private final int width; + private final int height; + private final ThumbnailStreamOpenerFactory factory; + private InputStream inputStream; + + public MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher defaultFetcher, + int width, int height) { + this(context, mediaStoreUri, defaultFetcher, width, height, DEFAULT_FACTORY); + } + + MediaStoreThumbFetcher(Context context, Uri mediaStoreUri, DataFetcher defaultFetcher, int width, + int height, ThumbnailStreamOpenerFactory factory) { + this.context = context; + this.mediaStoreUri = mediaStoreUri; + this.defaultFetcher = defaultFetcher; + this.width = width; + this.height = height; + this.factory = factory; + } + + @Override + public InputStream loadData(Priority priority) throws Exception { + ThumbnailStreamOpener fetcher = factory.build(mediaStoreUri, width, height); + + if (fetcher != null) { + inputStream = openThumbInputStream(fetcher); + } + + if (inputStream == null) { + inputStream = defaultFetcher.loadData(priority); + } + + return inputStream; + } + + private InputStream openThumbInputStream(ThumbnailStreamOpener fetcher) { + InputStream result = null; + try { + result = fetcher.open(context, mediaStoreUri); + } catch (FileNotFoundException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to find thumbnail file", e); + } + } + + int orientation = -1; + if (result != null) { + orientation = fetcher.getOrientation(context, mediaStoreUri); + } + + if (orientation != -1) { + result = new ExifOrientationStream(result, orientation); + } + return result; + } + + @Override + public void cleanup() { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // Do nothing. + } + } + defaultFetcher.cleanup(); + } + + @Override + public String getId() { + return mediaStoreUri.toString(); + } + + @Override + public void cancel() { + // Do nothing. + } + + private static boolean isMediaStoreUri(Uri uri) { + return uri != null + && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + && MediaStore.AUTHORITY.equals(uri.getAuthority()); + } + + private static boolean isMediaStoreVideo(Uri uri) { + return isMediaStoreUri(uri) && uri.getPathSegments().contains("video"); + } + + static class FileService { + public boolean exists(File file) { + return file.exists(); + } + + public long length(File file) { + return file.length(); + } + + public File get(String path) { + return new File(path); + } + } + + interface ThumbnailQuery { + Cursor queryPath(Context context, Uri uri); + } + + static class ThumbnailStreamOpener { + private static final FileService DEFAULT_SERVICE = new FileService(); + private final FileService service; + private ThumbnailQuery query; + + public ThumbnailStreamOpener(ThumbnailQuery query) { + this(DEFAULT_SERVICE, query); + } + + public ThumbnailStreamOpener(FileService service, ThumbnailQuery query) { + this.service = service; + this.query = query; + } + + public int getOrientation(Context context, Uri uri) { + int orientation = -1; + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(uri); + orientation = new ImageHeaderParser(is).getOrientation(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to open uri: " + uri, e); + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Ignored. + } + } + } + return orientation; + } + + public InputStream open(Context context, Uri uri) throws FileNotFoundException { + Uri thumbnailUri = null; + InputStream inputStream = null; + + final Cursor cursor = query.queryPath(context, uri); + try { + if (cursor != null && cursor.moveToFirst()) { + thumbnailUri = parseThumbUri(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + if (thumbnailUri != null) { + inputStream = context.getContentResolver().openInputStream(thumbnailUri); + } + return inputStream; + } + + private Uri parseThumbUri(Cursor cursor) { + Uri result = null; + String path = cursor.getString(0); + if (!TextUtils.isEmpty(path)) { + File file = service.get(path); + if (service.exists(file) && service.length(file) > 0) { + result = Uri.fromFile(file); + } + } + return result; + } + } + + static class ImageThumbnailQuery implements ThumbnailQuery { + private static final String[] PATH_PROJECTION = { + MediaStore.Images.Thumbnails.DATA, + }; + private static final String PATH_SELECTION = + MediaStore.Images.Thumbnails.KIND + " = " + MediaStore.Images.Thumbnails.MINI_KIND + + " AND " + MediaStore.Images.Thumbnails.IMAGE_ID + " = ?"; + + @Override + public Cursor queryPath(Context context, Uri uri) { + String imageId = uri.getLastPathSegment(); + return context.getContentResolver().query( + MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, + PATH_PROJECTION, + PATH_SELECTION, + new String[] { imageId }, + null /*sortOrder*/); + } + } + + static class VideoThumbnailQuery implements ThumbnailQuery { + private static final String[] PATH_PROJECTION = { + MediaStore.Video.Thumbnails.DATA + }; + private static final String PATH_SELECTION = + MediaStore.Video.Thumbnails.KIND + " = " + MediaStore.Video.Thumbnails.MINI_KIND + + " AND " + MediaStore.Video.Thumbnails.VIDEO_ID + " = ?"; + + @Override + public Cursor queryPath(Context context, Uri uri) { + String videoId = uri.getLastPathSegment(); + return context.getContentResolver().query( + MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI, + PATH_PROJECTION, + PATH_SELECTION, + new String[] { videoId }, + null /*sortOrder*/); + } + } + + static class ThumbnailStreamOpenerFactory { + + public ThumbnailStreamOpener build(Uri uri, int width, int height) { + if (!isMediaStoreUri(uri) || width > MINI_WIDTH || height > MINI_HEIGHT) { + return null; + } else if (isMediaStoreVideo(uri)) { + return new ThumbnailStreamOpener(new VideoThumbnailQuery()); + } else { + return new ThumbnailStreamOpener(new ImageThumbnailQuery()); + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/StreamAssetPathFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/StreamAssetPathFetcher.java new file mode 100755 index 0000000..2ccee55 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/StreamAssetPathFetcher.java @@ -0,0 +1,25 @@ +package com.example.bumptech.glide.load.data; + +import android.content.res.AssetManager; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Fetches an {@link InputStream} for an asset path. + */ +public class StreamAssetPathFetcher extends AssetPathFetcher { + public StreamAssetPathFetcher(AssetManager assetManager, String assetPath) { + super(assetManager, assetPath); + } + + @Override + protected InputStream loadResource(AssetManager assetManager, String path) throws IOException { + return assetManager.open(path); + } + + @Override + protected void close(InputStream data) throws IOException { + data.close(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/data/StreamLocalUriFetcher.java b/core/src/main/java/com/example/bumptech/glide/load/data/StreamLocalUriFetcher.java new file mode 100755 index 0000000..99bd893 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/data/StreamLocalUriFetcher.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.load.data; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Fetches an {@link InputStream} for a local {@link Uri}. + */ +public class StreamLocalUriFetcher extends LocalUriFetcher { + public StreamLocalUriFetcher(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { + return contentResolver.openInputStream(uri); + } + + @Override + protected void close(InputStream data) throws IOException { + data.close(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/CacheLoader.java b/core/src/main/java/com/example/bumptech/glide/load/engine/CacheLoader.java new file mode 100755 index 0000000..3414444 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/CacheLoader.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.engine; + +import android.util.Log; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.cache.DiskCache; + +import java.io.File; +import java.io.IOException; + +class CacheLoader { + private static final String TAG = "CacheLoader"; + private final DiskCache diskCache; + + public CacheLoader(DiskCache diskCache) { + this.diskCache = diskCache; + } + + public Resource load(Key key, ResourceDecoder decoder, int width, int height) { + File fromCache = diskCache.get(key); + if (fromCache == null) { + return null; + } + + Resource result = null; + try { + result = decoder.decode(fromCache, width, height); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Exception decoding image from cache", e); + } + } + if (result == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to decode image from cache or not present in cache"); + } + diskCache.delete(key); + } + return result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/DecodeJob.java b/core/src/main/java/com/example/bumptech/glide/load/engine/DecodeJob.java new file mode 100755 index 0000000..1eebedd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/DecodeJob.java @@ -0,0 +1,298 @@ +package com.example.bumptech.glide.load.engine; + +import android.util.Log; + + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.engine.cache.DiskCache; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.util.LogTime; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A class responsible for decoding resources either from cached data or from the original source and applying + * transformations and transcodes. + * + * @param
The type of the source data the resource can be decoded from. + * @param The type of resource that will be decoded. + * @param The type of resource that will be transcoded from the decoded and transformed resource. + */ +class DecodeJob { + private static final String TAG = "DecodeJob"; + private static final FileOpener DEFAULT_FILE_OPENER = new FileOpener(); + + private final EngineKey resultKey; + private final int width; + private final int height; + private final DataFetcher fetcher; + private final DataLoadProvider loadProvider; + private final Transformation transformation; + private final ResourceTranscoder transcoder; + private final DiskCacheProvider diskCacheProvider; + private final DiskCacheStrategy diskCacheStrategy; + private final Priority priority; + private final FileOpener fileOpener; + + private volatile boolean isCancelled; + + public DecodeJob(EngineKey resultKey, int width, int height, DataFetcher fetcher, + DataLoadProvider loadProvider, Transformation transformation, ResourceTranscoder transcoder, + DiskCacheProvider diskCacheProvider, DiskCacheStrategy diskCacheStrategy, Priority priority) { + this(resultKey, width, height, fetcher, loadProvider, transformation, transcoder, diskCacheProvider, + diskCacheStrategy, priority, DEFAULT_FILE_OPENER); + } + + // Visible for testing. + DecodeJob(EngineKey resultKey, int width, int height, DataFetcher fetcher, + DataLoadProvider loadProvider, Transformation transformation, ResourceTranscoder transcoder, + DiskCacheProvider diskCacheProvider, DiskCacheStrategy diskCacheStrategy, Priority priority, FileOpener + fileOpener) { + this.resultKey = resultKey; + this.width = width; + this.height = height; + this.fetcher = fetcher; + this.loadProvider = loadProvider; + this.transformation = transformation; + this.transcoder = transcoder; + this.diskCacheProvider = diskCacheProvider; + this.diskCacheStrategy = diskCacheStrategy; + this.priority = priority; + this.fileOpener = fileOpener; + } + + /** + * Returns a transcoded resource decoded from transformed resource data in the disk cache, or null if no such + * resource exists. + * + * @throws Exception + */ + public Resource decodeResultFromCache() throws Exception { + if (!diskCacheStrategy.cacheResult()) { + return null; + } + + long startTime = LogTime.getLogTime(); + Resource transformed = loadFromCache(resultKey); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Decoded transformed from cache", startTime); + } + startTime = LogTime.getLogTime(); + Resource result = transcode(transformed); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Transcoded transformed from cache", startTime); + } + return result; + } + + /** + * Returns a transformed and transcoded resource decoded from source data in the disk cache, or null if no such + * resource exists. + * + * @throws Exception + */ + public Resource decodeSourceFromCache() throws Exception { + if (!diskCacheStrategy.cacheSource()) { + return null; + } + + long startTime = LogTime.getLogTime(); + Resource decoded = loadFromCache(resultKey.getOriginalKey()); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Decoded source from cache", startTime); + } + return transformEncodeAndTranscode(decoded); + } + + /** + * Returns a transformed and transcoded resource decoded from source data, or null if no source data could be + * obtained or no resource could be decoded. + * + *

+ * Depending on the {@link DiskCacheStrategy} used, source data is either decoded + * directly or first written to the disk cache and then decoded from the disk cache. + *

+ * + * @throws Exception + */ + public Resource decodeFromSource() throws Exception { + Resource decoded = decodeSource(); + return transformEncodeAndTranscode(decoded); + } + + public void cancel() { + isCancelled = true; + fetcher.cancel(); + } + + private Resource transformEncodeAndTranscode(Resource decoded) { + long startTime = LogTime.getLogTime(); + Resource transformed = transform(decoded); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Transformed resource from source", startTime); + } + + writeTransformedToCache(transformed); + + startTime = LogTime.getLogTime(); + Resource result = transcode(transformed); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Transcoded transformed from source", startTime); + } + return result; + } + + private void writeTransformedToCache(Resource transformed) { + if (transformed == null || !diskCacheStrategy.cacheResult()) { + return; + } + long startTime = LogTime.getLogTime(); + SourceWriter> writer = new SourceWriter>(loadProvider.getEncoder(), transformed); + diskCacheProvider.getDiskCache().put(resultKey, writer); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Wrote transformed from source to cache", startTime); + } + } + + private Resource decodeSource() throws Exception { + Resource decoded = null; + try { + long startTime = LogTime.getLogTime(); + final A data = fetcher.loadData(priority); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Fetched data", startTime); + } + if (isCancelled) { + return null; + } + decoded = decodeFromSourceData(data); + } finally { + fetcher.cleanup(); + } + return decoded; + } + + private Resource decodeFromSourceData(A data) throws IOException { + final Resource decoded; + if (diskCacheStrategy.cacheSource()) { + decoded = cacheAndDecodeSourceData(data); + } else { + long startTime = LogTime.getLogTime(); + decoded = loadProvider.getSourceDecoder().decode(data, width, height); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Decoded from source", startTime); + } + } + return decoded; + } + + private Resource cacheAndDecodeSourceData(A data) throws IOException { + long startTime = LogTime.getLogTime(); + SourceWriter
writer = new SourceWriter(loadProvider.getSourceEncoder(), data); + diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Wrote source to cache", startTime); + } + + startTime = LogTime.getLogTime(); + Resource result = loadFromCache(resultKey.getOriginalKey()); + if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) { + logWithTimeAndKey("Decoded source from cache", startTime); + } + return result; + } + + private Resource loadFromCache(Key key) throws IOException { + File cacheFile = diskCacheProvider.getDiskCache().get(key); + if (cacheFile == null) { + return null; + } + + Resource result = null; + try { + result = loadProvider.getCacheDecoder().decode(cacheFile, width, height); + } finally { + if (result == null) { + diskCacheProvider.getDiskCache().delete(key); + } + } + return result; + } + + private Resource transform(Resource decoded) { + if (decoded == null) { + return null; + } + + Resource transformed = transformation.transform(decoded, width, height); + if (!decoded.equals(transformed)) { + decoded.recycle(); + } + return transformed; + } + + private Resource transcode(Resource transformed) { + if (transformed == null) { + return null; + } + return transcoder.transcode(transformed); + } + + private void logWithTimeAndKey(String message, long startTime) { + Log.v(TAG, message + " in " + LogTime.getElapsedMillis(startTime) + ", key: " + resultKey); + } + + class SourceWriter implements DiskCache.Writer { + + private final Encoder encoder; + private final DataType data; + + public SourceWriter(Encoder encoder, DataType data) { + this.encoder = encoder; + this.data = data; + } + + @Override + public boolean write(File file) { + boolean success = false; + OutputStream os = null; + try { + os = fileOpener.open(file); + success = encoder.encode(data, os); + } catch (FileNotFoundException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to find file to write to disk cache", e); + } + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + return success; + } + } + + interface DiskCacheProvider { + DiskCache getDiskCache(); + } + + static class FileOpener { + public OutputStream open(File file) throws FileNotFoundException { + return new BufferedOutputStream(new FileOutputStream(file)); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/DiskCacheStrategy.java b/core/src/main/java/com/example/bumptech/glide/load/engine/DiskCacheStrategy.java new file mode 100755 index 0000000..81cb4de --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/DiskCacheStrategy.java @@ -0,0 +1,37 @@ +package com.example.bumptech.glide.load.engine; + +/** + * Set of available caching strategies for media. + */ +public enum DiskCacheStrategy { + /** Caches with both {@link #SOURCE} and {@link #RESULT}. */ + ALL(true, true), + /** Saves no data to cache. */ + NONE(false, false), + /** Saves just the original data to cache. */ + SOURCE(true, false), + /** Saves the media item after all transformations to cache. */ + RESULT(false, true); + + private final boolean cacheSource; + private final boolean cacheResult; + + DiskCacheStrategy(boolean cacheSource, boolean cacheResult) { + this.cacheSource = cacheSource; + this.cacheResult = cacheResult; + } + + /** + * Returns true if this request should cache the original unmodified data. + */ + public boolean cacheSource() { + return cacheSource; + } + + /** + * Returns true if this request should cache the final transformed result. + */ + public boolean cacheResult() { + return cacheResult; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/Engine.java b/core/src/main/java/com/example/bumptech/glide/load/engine/Engine.java new file mode 100755 index 0000000..2a776ea --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/Engine.java @@ -0,0 +1,383 @@ +package com.example.bumptech.glide.load.engine; + +import android.os.Looper; +import android.os.MessageQueue; +import android.util.Log; + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.engine.cache.DiskCache; +import com.example.bumptech.glide.load.engine.cache.DiskCacheAdapter; +import com.example.bumptech.glide.load.engine.cache.MemoryCache; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.DataLoadProvider; +import com.example.bumptech.glide.request.ResourceCallback; +import com.example.bumptech.glide.util.LogTime; +import com.example.bumptech.glide.util.Util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +/** + * Responsible for starting loads and managing active and cached resources. + */ +public class Engine implements EngineJobListener, + MemoryCache.ResourceRemovedListener, + EngineResource.ResourceListener { + private static final String TAG = "Engine"; + private final Map jobs; + private final EngineKeyFactory keyFactory; + private final MemoryCache cache; + private final EngineJobFactory engineJobFactory; + private final Map>> activeResources; + private final ResourceRecycler resourceRecycler; + private final LazyDiskCacheProvider diskCacheProvider; + + // Lazily instantiate to avoid exceptions if Glide is initialized on a background thread. See #295. + private ReferenceQueue> resourceReferenceQueue; + + /** + * Allows a request to indicate it no longer is interested in a given load. + */ + public static class LoadStatus { + private final EngineJob engineJob; + private final ResourceCallback cb; + + public LoadStatus(ResourceCallback cb, EngineJob engineJob) { + this.cb = cb; + this.engineJob = engineJob; + } + + public void cancel() { + engineJob.removeCallback(cb); + } + } + + public Engine(MemoryCache memoryCache, DiskCache.Factory diskCacheFactory, ExecutorService diskCacheService, + ExecutorService sourceService) { + this(memoryCache, diskCacheFactory, diskCacheService, sourceService, null, null, null, null, null); + } + + // Visible for testing. + Engine(MemoryCache cache, DiskCache.Factory diskCacheFactory, ExecutorService diskCacheService, + ExecutorService sourceService, Map jobs, EngineKeyFactory keyFactory, + Map>> activeResources, EngineJobFactory engineJobFactory, + ResourceRecycler resourceRecycler) { + this.cache = cache; + this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory); + + if (activeResources == null) { + activeResources = new HashMap>>(); + } + this.activeResources = activeResources; + + if (keyFactory == null) { + keyFactory = new EngineKeyFactory(); + } + this.keyFactory = keyFactory; + + if (jobs == null) { + jobs = new HashMap(); + } + this.jobs = jobs; + + if (engineJobFactory == null) { + engineJobFactory = new EngineJobFactory(diskCacheService, sourceService, this); + } + this.engineJobFactory = engineJobFactory; + + if (resourceRecycler == null) { + resourceRecycler = new ResourceRecycler(); + } + this.resourceRecycler = resourceRecycler; + + cache.setResourceRemovedListener(this); + } + + /** + * Starts a load for the given arguments. Must be called on the main thread. + * + *

+ * The flow for any request is as follows: + *

    + *
  • Check the memory cache and provide the cached resource if present
  • + *
  • Check the current set of actively used resources and return the active resource if present
  • + *
  • Check the current set of in progress loads and add the cb to the in progress load if present
  • + *
  • Start a new load
  • + *
+ *

+ * + *

+ * Active resources are those that have been provided to at least one request and have not yet been released. + * Once all consumers of a resource have released that resource, the resource then goes to cache. If the + * resource is ever returned to a new consumer from cache, it is re-added to the active resources. If the + * resource is evicted from the cache, its resources are recycled and re-used if possible and the resource is + * discarded. There is no strict requirement that consumers release their resources so active resources are + * held weakly. + *

+ * + * @param signature A non-null unique key to be mixed into the cache key that identifies the version of the data to + * be loaded. + * @param width The target width in pixels of the desired resource. + * @param height The target height in pixels of the desired resource. + * @param fetcher The fetcher to use to retrieve data not in the disk cache. + * @param loadProvider The load provider containing various encoders and decoders use to decode and encode data. + * @param transformation The transformation to use to transform the decoded resource. + * @param transcoder The transcoder to use to transcode the decoded and transformed resource. + * @param priority The priority with which the request should run. + * @param isMemoryCacheable True if the transcoded resource can be cached in memory. + * @param diskCacheStrategy The strategy to use that determines what type of data, if any, + * will be cached in the local disk cache. + * @param cb The callback that will be called when the load completes. + * + * @param The type of data the resource will be decoded from. + * @param The type of the resource that will be decoded. + * @param The type of the resource that will be transcoded from the decoded resource. + */ + public LoadStatus load(Key signature, int width, int height, DataFetcher fetcher, + DataLoadProvider loadProvider, Transformation transformation, ResourceTranscoder transcoder, + Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) { + Util.assertMainThread(); + long startTime = LogTime.getLogTime(); + + final String id = fetcher.getId(); + EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(), + loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(), + transcoder, loadProvider.getSourceEncoder()); + + EngineResource cached = loadFromCache(key, isMemoryCacheable); + if (cached != null) { + cb.onResourceReady(cached); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Loaded resource from cache", startTime, key); + } + return null; + } + + EngineResource active = loadFromActiveResources(key, isMemoryCacheable); + if (active != null) { + cb.onResourceReady(active); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Loaded resource from active resources", startTime, key); + } + return null; + } + + EngineJob current = jobs.get(key); + if (current != null) { + current.addCallback(cb); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Added to existing load", startTime, key); + } + return new LoadStatus(cb, current); + } + + EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable); + DecodeJob decodeJob = new DecodeJob(key, width, height, fetcher, loadProvider, transformation, + transcoder, diskCacheProvider, diskCacheStrategy, priority); + EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority); + jobs.put(key, engineJob); + engineJob.addCallback(cb); + engineJob.start(runnable); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logWithTimeAndKey("Started new load", startTime, key); + } + return new LoadStatus(cb, engineJob); + } + + private static void logWithTimeAndKey(String log, long startTime, Key key) { + Log.v(TAG, log + " in " + LogTime.getElapsedMillis(startTime) + "ms, key: " + key); + } + + private EngineResource loadFromActiveResources(Key key, boolean isMemoryCacheable) { + if (!isMemoryCacheable) { + return null; + } + + EngineResource active = null; + WeakReference> activeRef = activeResources.get(key); + if (activeRef != null) { + active = activeRef.get(); + if (active != null) { + active.acquire(); + } else { + activeResources.remove(key); + } + } + + return active; + } + + private EngineResource loadFromCache(Key key, boolean isMemoryCacheable) { + if (!isMemoryCacheable) { + return null; + } + + EngineResource cached = getEngineResourceFromCache(key); + if (cached != null) { + cached.acquire(); + activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue())); + } + return cached; + } + + @SuppressWarnings("unchecked") + private EngineResource getEngineResourceFromCache(Key key) { + Resource cached = cache.remove(key); + + final EngineResource result; + if (cached == null) { + result = null; + } else if (cached instanceof EngineResource) { + // Save an object allocation if we've cached an EngineResource (the typical case). + result = (EngineResource) cached; + } else { + result = new EngineResource(cached, true /*isCacheable*/); + } + return result; + } + + public void release(Resource resource) { + Util.assertMainThread(); + if (resource instanceof EngineResource) { + ((EngineResource) resource).release(); + } else { + throw new IllegalArgumentException("Cannot release anything but an EngineResource"); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onEngineJobComplete(Key key, EngineResource resource) { + Util.assertMainThread(); + // A null resource indicates that the load failed, usually due to an exception. + if (resource != null) { + resource.setResourceListener(key, this); + + if (resource.isCacheable()) { + activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue())); + } + } + // TODO: should this check that the engine job is still current? + jobs.remove(key); + } + + @Override + public void onEngineJobCancelled(EngineJob engineJob, Key key) { + Util.assertMainThread(); + EngineJob current = jobs.get(key); + if (engineJob.equals(current)) { + jobs.remove(key); + } + } + + @Override + public void onResourceRemoved(final Resource resource) { + Util.assertMainThread(); + resourceRecycler.recycle(resource); + } + + @Override + public void onResourceReleased(Key cacheKey, EngineResource resource) { + Util.assertMainThread(); + activeResources.remove(cacheKey); + if (resource.isCacheable()) { + cache.put(cacheKey, resource); + } else { + resourceRecycler.recycle(resource); + } + } + + public void clearDiskCache() { + diskCacheProvider.getDiskCache().clear(); + } + + private ReferenceQueue> getReferenceQueue() { + if (resourceReferenceQueue == null) { + resourceReferenceQueue = new ReferenceQueue>(); + MessageQueue queue = Looper.myQueue(); + queue.addIdleHandler(new RefQueueIdleHandler(activeResources, resourceReferenceQueue)); + } + return resourceReferenceQueue; + } + + private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider { + + private final DiskCache.Factory factory; + private volatile DiskCache diskCache; + + public LazyDiskCacheProvider(DiskCache.Factory factory) { + this.factory = factory; + } + + @Override + public DiskCache getDiskCache() { + if (diskCache == null) { + synchronized (this) { + if (diskCache == null) { + diskCache = factory.build(); + } + if (diskCache == null) { + diskCache = new DiskCacheAdapter(); + } + } + } + return diskCache; + } + } + + private static class ResourceWeakReference extends WeakReference> { + private final Key key; + + public ResourceWeakReference(Key key, EngineResource r, ReferenceQueue> q) { + super(r, q); + this.key = key; + } + } + + // Responsible for cleaning up the active resource map by remove weak references that have been cleared. + private static class RefQueueIdleHandler implements MessageQueue.IdleHandler { + private final Map>> activeResources; + private final ReferenceQueue> queue; + + public RefQueueIdleHandler(Map>> activeResources, + ReferenceQueue> queue) { + this.activeResources = activeResources; + this.queue = queue; + } + + @Override + public boolean queueIdle() { + ResourceWeakReference ref = (ResourceWeakReference) queue.poll(); + if (ref != null) { + activeResources.remove(ref.key); + } + + return true; + } + } + + // Visible for testing. + static class EngineJobFactory { + private final ExecutorService diskCacheService; + private final ExecutorService sourceService; + private final EngineJobListener listener; + + public EngineJobFactory(ExecutorService diskCacheService, ExecutorService sourceService, + EngineJobListener listener) { + this.diskCacheService = diskCacheService; + this.sourceService = sourceService; + this.listener = listener; + } + + public EngineJob build(Key key, boolean isMemoryCacheable) { + return new EngineJob(key, diskCacheService, sourceService, isMemoryCacheable, listener); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJob.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJob.java new file mode 100755 index 0000000..d6db672 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJob.java @@ -0,0 +1,213 @@ +package com.example.bumptech.glide.load.engine; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.request.ResourceCallback; +import com.example.bumptech.glide.util.Util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * A class that manages a load by adding and removing callbacks for for the load and notifying callbacks when the + * load completes. + */ +class EngineJob implements EngineRunnable.EngineRunnableManager { + private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory(); + private static final Handler MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper(), new MainThreadCallback()); + + private static final int MSG_COMPLETE = 1; + private static final int MSG_EXCEPTION = 2; + + private final List cbs = new ArrayList(); + private final EngineResourceFactory engineResourceFactory; + private final EngineJobListener listener; + private final Key key; + private final ExecutorService diskCacheService; + private final ExecutorService sourceService; + private final boolean isCacheable; + + private boolean isCancelled; + // Either resource or exception (particularly exception) may be returned to us null, so use booleans to track if + // we've received them instead of relying on them to be non-null. See issue #180. + private Resource resource; + private boolean hasResource; + private Exception exception; + private boolean hasException; + // A set of callbacks that are removed while we're notifying other callbacks of a change in status. + private Set ignoredCallbacks; + private EngineRunnable engineRunnable; + private EngineResource engineResource; + + private volatile Future future; + + public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, + EngineJobListener listener) { + this(key, diskCacheService, sourceService, isCacheable, listener, DEFAULT_FACTORY); + } + + public EngineJob(Key key, ExecutorService diskCacheService, ExecutorService sourceService, boolean isCacheable, + EngineJobListener listener, EngineResourceFactory engineResourceFactory) { + this.key = key; + this.diskCacheService = diskCacheService; + this.sourceService = sourceService; + this.isCacheable = isCacheable; + this.listener = listener; + this.engineResourceFactory = engineResourceFactory; + } + + public void start(EngineRunnable engineRunnable) { + this.engineRunnable = engineRunnable; + future = diskCacheService.submit(engineRunnable); + } + + @Override + public void submitForSource(EngineRunnable runnable) { + future = sourceService.submit(runnable); + } + + public void addCallback(ResourceCallback cb) { + Util.assertMainThread(); + if (hasResource) { + cb.onResourceReady(engineResource); + } else if (hasException) { + cb.onException(exception); + } else { + cbs.add(cb); + } + } + + public void removeCallback(ResourceCallback cb) { + Util.assertMainThread(); + if (hasResource || hasException) { + addIgnoredCallback(cb); + } else { + cbs.remove(cb); + if (cbs.isEmpty()) { + cancel(); + } + } + } + + // We cannot remove callbacks while notifying our list of callbacks directly because doing so would cause a + // ConcurrentModificationException. However, we need to obey the cancellation request such that if notifying a + // callback early in the callbacks list cancels a callback later in the request list, the cancellation for the later + // request is still obeyed. Using a set of ignored callbacks allows us to avoid the exception while still meeting + // the requirement. + private void addIgnoredCallback(ResourceCallback cb) { + if (ignoredCallbacks == null) { + ignoredCallbacks = new HashSet(); + } + ignoredCallbacks.add(cb); + } + + private boolean isInIgnoredCallbacks(ResourceCallback cb) { + return ignoredCallbacks != null && ignoredCallbacks.contains(cb); + } + + // Exposed for testing. + void cancel() { + if (hasException || hasResource || isCancelled) { + return; + } + engineRunnable.cancel(); + Future currentFuture = future; + if (currentFuture != null) { + currentFuture.cancel(true); + } + isCancelled = true; + listener.onEngineJobCancelled(this, key); + } + + // Exposed for testing. + boolean isCancelled() { + return isCancelled; + } + + @Override + public void onResourceReady(final Resource resource) { + this.resource = resource; + MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); + } + + private void handleResultOnMainThread() { + if (isCancelled) { + resource.recycle(); + return; + } else if (cbs.isEmpty()) { + throw new IllegalStateException("Received a resource without any callbacks to notify"); + } + engineResource = engineResourceFactory.build(resource, isCacheable); + hasResource = true; + + // Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it + // synchronously released by one of the callbacks. + engineResource.acquire(); + listener.onEngineJobComplete(key, engineResource); + + for (ResourceCallback cb : cbs) { + if (!isInIgnoredCallbacks(cb)) { + engineResource.acquire(); + cb.onResourceReady(engineResource); + } + } + // Our request is complete, so we can release the resource. + engineResource.release(); + } + + @Override + public void onException(final Exception e) { + this.exception = e; + MAIN_THREAD_HANDLER.obtainMessage(MSG_EXCEPTION, this).sendToTarget(); + } + + private void handleExceptionOnMainThread() { + if (isCancelled) { + return; + } else if (cbs.isEmpty()) { + throw new IllegalStateException("Received an exception without any callbacks to notify"); + } + hasException = true; + + listener.onEngineJobComplete(key, null); + + for (ResourceCallback cb : cbs) { + if (!isInIgnoredCallbacks(cb)) { + cb.onException(exception); + } + } + } + + // Visible for testing. + static class EngineResourceFactory { + public EngineResource build(Resource resource, boolean isMemoryCacheable) { + return new EngineResource(resource, isMemoryCacheable); + } + } + + private static class MainThreadCallback implements Handler.Callback { + + @Override + public boolean handleMessage(Message message) { + if (MSG_COMPLETE == message.what || MSG_EXCEPTION == message.what) { + EngineJob job = (EngineJob) message.obj; + if (MSG_COMPLETE == message.what) { + job.handleResultOnMainThread(); + } else { + job.handleExceptionOnMainThread(); + } + return true; + } + + return false; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJobListener.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJobListener.java new file mode 100755 index 0000000..77de21d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineJobListener.java @@ -0,0 +1,11 @@ +package com.example.bumptech.glide.load.engine; + + +import com.example.bumptech.glide.load.Key; + +interface EngineJobListener { + + void onEngineJobComplete(Key key, EngineResource resource); + + void onEngineJobCancelled(EngineJob engineJob, Key key); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKey.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKey.java new file mode 100755 index 0000000..475baaf --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKey.java @@ -0,0 +1,176 @@ +package com.example.bumptech.glide.load.engine; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +@SuppressWarnings("rawtypes") +class EngineKey implements Key { + private static final String EMPTY_LOG_STRING = ""; + private final String id; + private final int width; + private final int height; + private final ResourceDecoder cacheDecoder; + private final ResourceDecoder decoder; + private final Transformation transformation; + private final ResourceEncoder encoder; + private final ResourceTranscoder transcoder; + private final Encoder sourceEncoder; + private final Key signature; + private String stringKey; + private int hashCode; + private Key originalKey; + + public EngineKey(String id, Key signature, int width, int height, ResourceDecoder cacheDecoder, + ResourceDecoder decoder, Transformation transformation, ResourceEncoder encoder, + ResourceTranscoder transcoder, Encoder sourceEncoder) { + this.id = id; + this.signature = signature; + this.width = width; + this.height = height; + this.cacheDecoder = cacheDecoder; + this.decoder = decoder; + this.transformation = transformation; + this.encoder = encoder; + this.transcoder = transcoder; + this.sourceEncoder = sourceEncoder; + } + + public Key getOriginalKey() { + if (originalKey == null) { + originalKey = new OriginalKey(id, signature); + } + return originalKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EngineKey engineKey = (EngineKey) o; + + if (!id.equals(engineKey.id)) { + return false; + } else if (!signature.equals(engineKey.signature)) { + return false; + } else if (height != engineKey.height) { + return false; + } else if (width != engineKey.width) { + return false; + } else if (transformation == null ^ engineKey.transformation == null) { + return false; + } else if (transformation != null && !transformation.getId().equals(engineKey.transformation.getId())) { + return false; + } else if (decoder == null ^ engineKey.decoder == null) { + return false; + } else if (decoder != null && !decoder.getId().equals(engineKey.decoder.getId())) { + return false; + } else if (cacheDecoder == null ^ engineKey.cacheDecoder == null) { + return false; + } else if (cacheDecoder != null && !cacheDecoder.getId().equals(engineKey.cacheDecoder.getId())) { + return false; + } else if (encoder == null ^ engineKey.encoder == null) { + return false; + } else if (encoder != null && !encoder.getId().equals(engineKey.encoder.getId())) { + return false; + } else if (transcoder == null ^ engineKey.transcoder == null) { + return false; + } else if (transcoder != null && !transcoder.getId().equals(engineKey.transcoder.getId())) { + return false; + } else if (sourceEncoder == null ^ engineKey.sourceEncoder == null) { + return false; + } else if (sourceEncoder != null && !sourceEncoder.getId().equals(engineKey.sourceEncoder.getId())) { + return false; + } + return true; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = id.hashCode(); + hashCode = 31 * hashCode + signature.hashCode(); + hashCode = 31 * hashCode + width; + hashCode = 31 * hashCode + height; + hashCode = 31 * hashCode + (cacheDecoder != null ? cacheDecoder .getId().hashCode() : 0); + hashCode = 31 * hashCode + (decoder != null ? decoder .getId().hashCode() : 0); + hashCode = 31 * hashCode + (transformation != null ? transformation.getId().hashCode() : 0); + hashCode = 31 * hashCode + (encoder != null ? encoder .getId().hashCode() : 0); + hashCode = 31 * hashCode + (transcoder != null ? transcoder .getId().hashCode() : 0); + hashCode = 31 * hashCode + (sourceEncoder != null ? sourceEncoder .getId().hashCode() : 0); + } + return hashCode; + } + + @Override + public String toString() { + if (stringKey == null) { + stringKey = new StringBuilder() + .append("EngineKey{") + .append(id) + .append('+') + .append(signature) + .append("+[") + .append(width) + .append('x') + .append(height) + .append("]+") + .append('\'') + .append(cacheDecoder != null ? cacheDecoder .getId() : EMPTY_LOG_STRING) + .append('\'') + .append('+') + .append('\'') + .append(decoder != null ? decoder .getId() : EMPTY_LOG_STRING) + .append('\'') + .append('+') + .append('\'') + .append(transformation != null ? transformation.getId() : EMPTY_LOG_STRING) + .append('\'') + .append('+') + .append('\'') + .append(encoder != null ? encoder .getId() : EMPTY_LOG_STRING) + .append('\'') + .append('+') + .append('\'') + .append(transcoder != null ? transcoder .getId() : EMPTY_LOG_STRING) + .append('\'') + .append('+') + .append('\'') + .append(sourceEncoder != null ? sourceEncoder .getId() : EMPTY_LOG_STRING) + .append('\'') + .append('}') + .toString(); + } + return stringKey; + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + byte[] dimensions = ByteBuffer.allocate(8) + .putInt(width) + .putInt(height) + .array(); + signature.updateDiskCacheKey(messageDigest); + messageDigest.update(id.getBytes(STRING_CHARSET_NAME)); + messageDigest.update(dimensions); + messageDigest.update((cacheDecoder != null ? cacheDecoder .getId() : "").getBytes(STRING_CHARSET_NAME)); + messageDigest.update((decoder != null ? decoder .getId() : "").getBytes(STRING_CHARSET_NAME)); + messageDigest.update((transformation != null ? transformation.getId() : "").getBytes(STRING_CHARSET_NAME)); + messageDigest.update((encoder != null ? encoder .getId() : "").getBytes(STRING_CHARSET_NAME)); + // The Transcoder is not included in the disk cache key because its result is not cached. + messageDigest.update((sourceEncoder != null ? sourceEncoder .getId() : "").getBytes(STRING_CHARSET_NAME)); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKeyFactory.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKeyFactory.java new file mode 100755 index 0000000..8bdee7d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineKeyFactory.java @@ -0,0 +1,21 @@ +package com.example.bumptech.glide.load.engine; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +class EngineKeyFactory { + + @SuppressWarnings("rawtypes") + public EngineKey buildKey(String id, Key signature, int width, int height, ResourceDecoder cacheDecoder, + ResourceDecoder sourceDecoder, Transformation transformation, ResourceEncoder encoder, + ResourceTranscoder transcoder, Encoder sourceEncoder) { + return new EngineKey(id, signature, width, height, cacheDecoder, sourceDecoder, transformation, encoder, + transcoder, sourceEncoder); + } + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineResource.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineResource.java new file mode 100755 index 0000000..2a4bdc5 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineResource.java @@ -0,0 +1,104 @@ +package com.example.bumptech.glide.load.engine; + +import android.os.Looper; + +import com.example.bumptech.glide.load.Key; + + +/** + * A wrapper resource that allows reference counting a wrapped {@link Resource} + * interface. + * + * @param The type of data returned by the wrapped {@link Resource}. + */ +class EngineResource implements Resource { + private final Resource resource; + private final boolean isCacheable; + private ResourceListener listener; + private Key key; + private int acquired; + private boolean isRecycled; + + interface ResourceListener { + void onResourceReleased(Key key, EngineResource resource); + } + + EngineResource(Resource toWrap, boolean isCacheable) { + if (toWrap == null) { + throw new NullPointerException("Wrapped resource must not be null"); + } + resource = toWrap; + this.isCacheable = isCacheable; + } + + void setResourceListener(Key key, ResourceListener listener) { + this.key = key; + this.listener = listener; + } + + boolean isCacheable() { + return isCacheable; + } + + @Override + public Z get() { + return resource.get(); + } + + @Override + public int getSize() { + return resource.getSize(); + } + + @Override + public void recycle() { + if (acquired > 0) { + throw new IllegalStateException("Cannot recycle a resource while it is still acquired"); + } + if (isRecycled) { + throw new IllegalStateException("Cannot recycle a resource that has already been recycled"); + } + isRecycled = true; + resource.recycle(); + } + + /** + * Increments the number of consumers using the wrapped resource. Must be called on the main thread. + * + *

+ * This must be called with a number corresponding to the number of new consumers each time new consumers + * begin using the wrapped resource. It is always safer to call acquire more often than necessary. Generally + * external users should never call this method, the framework will take care of this for you. + *

+ */ + void acquire() { + if (isRecycled) { + throw new IllegalStateException("Cannot acquire a recycled resource"); + } + if (!Looper.getMainLooper().equals(Looper.myLooper())) { + throw new IllegalThreadStateException("Must call acquire on the main thread"); + } + ++acquired; + } + + /** + * Decrements the number of consumers using the wrapped resource. Must be called on the main thread. + * + *

+ * This must only be called when a consumer that called the {@link #acquire()} method is now done with the + * resource. Generally external users should never callthis method, the framework will take care of this for + * you. + *

+ */ + void release() { + if (acquired <= 0) { + throw new IllegalStateException("Cannot release a recycled or not yet acquired resource"); + } + if (!Looper.getMainLooper().equals(Looper.myLooper())) { + throw new IllegalThreadStateException("Must call release on the main thread"); + } + if (--acquired == 0) { + listener.onResourceReleased(key, this); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/EngineRunnable.java b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineRunnable.java new file mode 100755 index 0000000..85daef6 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/EngineRunnable.java @@ -0,0 +1,141 @@ +package com.example.bumptech.glide.load.engine; + +import android.util.Log; + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.engine.executor.Prioritized; +import com.example.bumptech.glide.request.ResourceCallback; + + +/** + * A runnable class responsible for using an {@link DecodeJob} to decode resources on a + * background thread in two stages. + * + *

+ * In the first stage, this class attempts to decode a resource + * from cache, first using transformed data and then using source data. If no resource can be decoded from cache, + * this class then requests to be posted again. During the second stage this class then attempts to use the + * {@link DecodeJob} to decode data directly from the original source. + *

+ * + *

+ * Using two stages with a re-post in between allows us to run fast disk cache decodes on one thread and slow source + * fetches on a second pool so that loads for local data are never blocked waiting for loads for remote data to + * complete. + *

+ */ +class EngineRunnable implements Runnable, Prioritized { + private static final String TAG = "EngineRunnable"; + + private final Priority priority; + private final EngineRunnableManager manager; + private final DecodeJob decodeJob; + + private Stage stage; + + private volatile boolean isCancelled; + + public EngineRunnable(EngineRunnableManager manager, DecodeJob decodeJob, Priority priority) { + this.manager = manager; + this.decodeJob = decodeJob; + this.stage = Stage.CACHE; + this.priority = priority; + } + + public void cancel() { + isCancelled = true; + decodeJob.cancel(); + } + + @Override + public void run() { + if (isCancelled) { + return; + } + + Exception exception = null; + Resource resource = null; + try { + resource = decode(); + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Exception decoding", e); + } + exception = e; + } + + if (isCancelled) { + if (resource != null) { + resource.recycle(); + } + return; + } + + if (resource == null) { + onLoadFailed(exception); + } else { + onLoadComplete(resource); + } + } + + private boolean isDecodingFromCache() { + return stage == Stage.CACHE; + } + + private void onLoadComplete(Resource resource) { + manager.onResourceReady(resource); + } + + private void onLoadFailed(Exception e) { + if (isDecodingFromCache()) { + stage = Stage.SOURCE; + manager.submitForSource(this); + } else { + manager.onException(e); + } + } + + private Resource decode() throws Exception { + if (isDecodingFromCache()) { + return decodeFromCache(); + } else { + return decodeFromSource(); + } + } + + private Resource decodeFromCache() throws Exception { + Resource result = null; + try { + result = decodeJob.decodeResultFromCache(); + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Exception decoding result from cache: " + e); + } + } + + if (result == null) { + result = decodeJob.decodeSourceFromCache(); + } + return result; + } + + private Resource decodeFromSource() throws Exception { + return decodeJob.decodeFromSource(); + } + + @Override + public int getPriority() { + return priority.ordinal(); + } + + private enum Stage { + /** Attempting to decode resource from cache. */ + CACHE, + /** Attempting to decode resource from source data. */ + SOURCE + } + + interface EngineRunnableManager extends ResourceCallback { + void submitForSource(EngineRunnable runnable); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/OriginalKey.java b/core/src/main/java/com/example/bumptech/glide/load/engine/OriginalKey.java new file mode 100755 index 0000000..0d79e4c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/OriginalKey.java @@ -0,0 +1,55 @@ +package com.example.bumptech.glide.load.engine; + + +import com.example.bumptech.glide.load.Key; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; + +/** + * A class for keeping track of the cache key of the original data + any requested signature. + */ +public class OriginalKey implements Key { + + private final String id; + private final Key signature; + + public OriginalKey(String id, Key signature) { + this.id = id; + this.signature = signature; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + OriginalKey that = (OriginalKey) o; + + if (!id.equals(that.id)) { + return false; + } + if (!signature.equals(that.signature)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + signature.hashCode(); + return result; + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + messageDigest.update(id.getBytes(STRING_CHARSET_NAME)); + signature.updateDiskCacheKey(messageDigest); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/Resource.java b/core/src/main/java/com/example/bumptech/glide/load/engine/Resource.java new file mode 100755 index 0000000..4a8ae83 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/Resource.java @@ -0,0 +1,48 @@ +package com.example.bumptech.glide.load.engine; + + +/** + * A resource interface that wraps a particular type so that it can be pooled and reused. + * + * @param The type of resource wrapped by this class. + */ +public interface Resource { + + /** + * Returns an instance of the wrapped resource. + *

+ * Note - This does not have to be the same instance of the wrapped resource class and in fact it is often + * appropriate to return a new instance for each call. For example, + * {@link android.graphics.drawable.Drawable Drawable}s should only be used by a single + * {@link android.view.View View} at a time so each call to this method for Resources that wrap + * {@link android.graphics.drawable.Drawable Drawable}s should always return a new + * {@link android.graphics.drawable.Drawable Drawable}. + *

+ */ + Z get(); + + /** + * Returns the size in bytes of the wrapped resource to use to determine how much of the memory cache this resource + * uses. + */ + int getSize(); + + /** + * Cleans up and recycles internal resources. + * + *

+ * It is only safe to call this method if there are no current resource consumers and if this method has not + * yet been called. Typically this occurs at one of two times: + *

    + *
  • During a resource load when the resource is transformed or transcoded before any consumer have + * ever had access to this resource
  • + *
  • After all consumers have released this resource and it has been evicted from the cache
  • + *
+ * + * For most users of this class, the only time this method should ever be called is during transformations or + * transcoders, the framework will call this method when all consumers have released this resource and it has + * been evicted from the cache. + *

+ */ + void recycle(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/ResourceRecycler.java b/core/src/main/java/com/example/bumptech/glide/load/engine/ResourceRecycler.java new file mode 100755 index 0000000..0184ddc --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/ResourceRecycler.java @@ -0,0 +1,44 @@ +package com.example.bumptech.glide.load.engine; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import com.example.bumptech.glide.util.Util; + + +/** + * A class that can safely recycle recursive resources. + */ +class ResourceRecycler { + private boolean isRecycling; + private final Handler handler = new Handler(Looper.getMainLooper(), new ResourceRecyclerCallback()); + + public void recycle(Resource resource) { + Util.assertMainThread(); + + if (isRecycling) { + // If a resource has sub-resources, releasing a sub resource can cause it's parent to be synchronously + // evicted which leads to a recycle loop when the parent releases it's children. Posting breaks this loop. + handler.obtainMessage(ResourceRecyclerCallback.RECYCLE_RESOURCE, resource).sendToTarget(); + } else { + isRecycling = true; + resource.recycle(); + isRecycling = false; + } + } + + private static class ResourceRecyclerCallback implements Handler.Callback { + public static final int RECYCLE_RESOURCE = 1; + + @Override + public boolean handleMessage(Message message) { + if (message.what == RECYCLE_RESOURCE) { + Resource resource = (Resource) message.obj; + resource.recycle(); + return true; + } + return false; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategy.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategy.java new file mode 100755 index 0000000..249b74b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/AttributeStrategy.java @@ -0,0 +1,122 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.util.Util; + + +/** + * A strategy for reusing bitmaps that requires any returned bitmap's dimensions to exactly match those request. + */ +class AttributeStrategy implements LruPoolStrategy { + private final KeyPool keyPool = new KeyPool(); + private final GroupedLinkedMap groupedMap = new GroupedLinkedMap(); + + public void put(Bitmap bitmap) { + final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + + groupedMap.put(key, bitmap); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + final Key key = keyPool.get(width, height, config); + + return groupedMap.get(key); + } + + @Override + public Bitmap removeLast() { + return groupedMap.removeLast(); + } + + @Override + public String logBitmap(Bitmap bitmap) { + return getBitmapString(bitmap); + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + return getBitmapString(width, height, config); + } + + @Override + public int getSize(Bitmap bitmap) { + return Util.getBitmapByteSize(bitmap); + } + + @Override + public String toString() { + return "AttributeStrategy:\n " + groupedMap; + } + + private static String getBitmapString(Bitmap bitmap) { + return getBitmapString(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig()); + } + + private static String getBitmapString(int width, int height, Bitmap.Config config) { + return "[" + width + "x" + height + "], " + config; + } + + // Visible for testing. + static class KeyPool extends BaseKeyPool { + public Key get(int width, int height, Bitmap.Config config) { + Key result = get(); + result.init(width, height, config); + return result; + } + + @Override + protected Key create() { + return new Key(this); + } + } + + // Visible for testing. + static class Key implements Poolable { + private final KeyPool pool; + private int width; + private int height; + // Config can be null :( + private Bitmap.Config config; + + public Key(KeyPool pool) { + this.pool = pool; + } + + public void init(int width, int height, Bitmap.Config config) { + this.width = width; + this.height = height; + this.config = config; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key other = (Key) o; + return width == other.width + && height == other.height + && config == other.config; + } + return false; + } + + @Override + public int hashCode() { + int result = width; + result = 31 * result + height; + result = 31 * result + (config != null ? config.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return getBitmapString(width, height, config); + } + + @Override + public void offer() { + pool.offer(this); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BaseKeyPool.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BaseKeyPool.java new file mode 100755 index 0000000..b285819 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BaseKeyPool.java @@ -0,0 +1,27 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + + +import com.example.bumptech.glide.util.Util; + +import java.util.Queue; + +abstract class BaseKeyPool { + private static final int MAX_SIZE = 20; + private final Queue keyPool = Util.createQueue(MAX_SIZE); + + protected T get() { + T result = keyPool.poll(); + if (result == null) { + result = create(); + } + return result; + } + + public void offer(T key) { + if (keyPool.size() < MAX_SIZE) { + keyPool.offer(key); + } + } + + protected abstract T create(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPool.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPool.java new file mode 100755 index 0000000..9060b01 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPool.java @@ -0,0 +1,114 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.graphics.Bitmap; + +/** + * An interface for a pool that allows users to reuse {@link Bitmap} objects. + */ +public interface BitmapPool { + + /** + * Returns the current maximum size of the pool in bytes. + */ + int getMaxSize(); + + /** + * Multiplies the initial size of the pool by the given multipler to dynamically and synchronously allow users to + * adjust the size of the pool. + * + *

+ * If the current total size of the pool is larger than the max size after the given multiplier is applied, + * {@link Bitmap}s should be evicted until the pool is smaller than the new max size. + *

+ * + * @param sizeMultiplier The size multiplier to apply between 0 and 1. + */ + void setSizeMultiplier(float sizeMultiplier); + + /** + * Adds the given {@link Bitmap} and returns {@code true} if the {@link Bitmap} + * was eligible to be added and {@code false} otherwise. + * + *

+ * Note - If the {@link Bitmap} is rejected (this method returns false) then it is the caller's + * responsibility to call {@link Bitmap#recycle()}. + *

+ * + *

+ * Note - This method will return {@code true} if the given {@link Bitmap} is synchronously + * evicted after being accepted. The only time this method will return {@code false} is if the + * {@link Bitmap} is not eligible to be added to the pool (either it is not mutable or it is + * larger than the max pool size). + *

+ * + * @see Bitmap#isMutable() + * @see Bitmap#recycle() + * + * @param bitmap The {@link Bitmap} to attempt to add. + */ + boolean put(Bitmap bitmap); + + /** + * Returns a {@link Bitmap} of exactly the given width, height, and configuration, and containing + * only transparent pixels or null if no such {@link Bitmap} could be obtained from the pool. + * + *

+ * Because this method erases all pixels in the {@link Bitmap}, this method is slightly slower than + * {@link #getDirty(int, int, Bitmap.Config)}. If the {@link Bitmap} is being + * obtained to be used in {@link android.graphics.BitmapFactory} or in any other case where every pixel in the + * {@link Bitmap} will always be overwritten or cleared, + * {@link #getDirty(int, int, Bitmap.Config)} will be faster. When in doubt, use this method + * to ensure correctness. + *

+ * + *
+     *     Implementations can should clear out every returned Bitmap using the following:
+     *
+     * {@code
+     * bitmap.eraseColor(Color.TRANSPARENT);
+     * }
+     * 
+ * + * @see #getDirty(int, int, Bitmap.Config) + * + * @param width The width in pixels of the desired {@link Bitmap}. + * @param height The height in pixels of the desired {@link Bitmap}. + * @param config The {@link Bitmap.Config} of the desired {@link Bitmap}. + */ + Bitmap get(int width, int height, Bitmap.Config config); + + /** + * Identical to {@link #get(int, int, Bitmap.Config)} except that any returned non-null + * {@link Bitmap} may not have been erased and may contain random data. + * + *

+ * Although this method is slightly more efficient than {@link #get(int, int, Bitmap.Config)} + * it should be used with caution and only when the caller is sure that they are going to erase the + * {@link Bitmap} entirely before writing new data to it. + *

+ * + * @see #get(int, int, Bitmap.Config) + * + * @param width The width in pixels of the desired {@link Bitmap}. + * @param height The height in pixels of the desired {@link Bitmap}. + * @param config The {@link Bitmap.Config} of the desired {@link Bitmap}. + * @return A {@link Bitmap} with exactly the given width, height, and config potentially containing + * random image data or null if no such {@link Bitmap} could be obtained from the pool. + */ + Bitmap getDirty(int width, int height, Bitmap.Config config); + + /** + * Removes all {@link Bitmap}s from the pool. + */ + void clearMemory(); + + /** + * Reduces the size of the cache by evicting items based on the given level. + * + * @see android.content.ComponentCallbacks2 + * + * @param level The level from {@link android.content.ComponentCallbacks2} to use to determine how many + * {@link Bitmap}s to evict. + */ + void trimMemory(int level); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPoolAdapter.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPoolAdapter.java new file mode 100755 index 0000000..36c5ae0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/BitmapPoolAdapter.java @@ -0,0 +1,44 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.graphics.Bitmap; + +/** + * An {@link BitmapPool BitmapPool} implementation that rejects all + * {@link Bitmap Bitmap}s added to it and always returns {@code null} from get. + */ +public class BitmapPoolAdapter implements BitmapPool { + @Override + public int getMaxSize() { + return 0; + } + + @Override + public void setSizeMultiplier(float sizeMultiplier) { + // Do nothing. + } + + @Override + public boolean put(Bitmap bitmap) { + return false; + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + return null; + } + + @Override + public Bitmap getDirty(int width, int height, Bitmap.Config config) { + return null; + } + + @Override + public void clearMemory() { + // Do nothing. + } + + @Override + public void trimMemory(int level) { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMap.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMap.java new file mode 100755 index 0000000..8dea93e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/GroupedLinkedMap.java @@ -0,0 +1,146 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Similar to {@link java.util.LinkedHashMap} when access ordered except that it is access ordered on groups + * of bitmaps rather than individual objects. The idea is to be able to find the LRU bitmap size, rather than the + * LRU bitmap object. We can then remove bitmaps from the least recently used size of bitmap when we need to + * reduce our cache size. + * + * For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if no bitmaps + * of that size are present. We do not count addition or removal of bitmaps as an access. + */ +class GroupedLinkedMap { + private final LinkedEntry head = new LinkedEntry(); + private final Map> keyToEntry = new HashMap>(); + + public void put(K key, V value) { + LinkedEntry entry = keyToEntry.get(key); + + if (entry == null) { + entry = new LinkedEntry(key); + makeTail(entry); + keyToEntry.put(key, entry); + } else { + key.offer(); + } + + entry.add(value); + } + + public V get(K key) { + LinkedEntry entry = keyToEntry.get(key); + if (entry == null) { + entry = new LinkedEntry(key); + keyToEntry.put(key, entry); + } else { + key.offer(); + } + + makeHead(entry); + + return entry.removeLast(); + } + + public V removeLast() { + LinkedEntry last = head.prev; + + while (!last.equals(head)) { + V removed = last.removeLast(); + if (removed != null) { + return removed; + } else { + // We will clean up empty lru entries since they are likely to have been one off or unusual sizes and + // are not likely to be requested again so the gc thrash should be minimal. Doing so will speed up our + // removeLast operation in the future and prevent our linked list from growing to arbitrarily large + // sizes. + removeEntry(last); + keyToEntry.remove(last.key); + last.key.offer(); + } + + last = last.prev; + } + + return null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("GroupedLinkedMap( "); + LinkedEntry current = head.next; + boolean hadAtLeastOneItem = false; + while (!current.equals(head)) { + hadAtLeastOneItem = true; + sb.append('{').append(current.key).append(':').append(current.size()).append("}, "); + current = current.next; + } + if (hadAtLeastOneItem) { + sb.delete(sb.length() - 2, sb.length()); + } + return sb.append(" )").toString(); + } + + // Make the entry the most recently used item. + private void makeHead(LinkedEntry entry) { + removeEntry(entry); + entry.prev = head; + entry.next = head.next; + updateEntry(entry); + } + + // Make the entry the least recently used item. + private void makeTail(LinkedEntry entry) { + removeEntry(entry); + entry.prev = head.prev; + entry.next = head; + updateEntry(entry); + } + + private static void updateEntry(LinkedEntry entry) { + entry.next.prev = entry; + entry.prev.next = entry; + } + + private static void removeEntry(LinkedEntry entry) { + entry.prev.next = entry.next; + entry.next.prev = entry.prev; + } + + private static class LinkedEntry { + private final K key; + private List values; + LinkedEntry next; + LinkedEntry prev; + + // Used only for the first item in the list which we will treat specially and which will not contain a value. + public LinkedEntry() { + this(null); + } + + public LinkedEntry(K key) { + next = prev = this; + this.key = key; + } + + public V removeLast() { + final int valueSize = size(); + return valueSize > 0 ? values.remove(valueSize - 1) : null; + } + + public int size() { + return values != null ? values.size() : 0; + } + + public void add(V value) { + if (values == null) { + values = new ArrayList(); + } + values.add(value); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPool.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPool.java new file mode 100755 index 0000000..d8f787e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruBitmapPool.java @@ -0,0 +1,271 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Build; +import android.util.Log; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * An {@link BitmapPool} implementation that uses an + * {@link LruPoolStrategy} to bucket {@link Bitmap}s and then uses an LRU + * eviction policy to evict {@link Bitmap}s from the least recently used bucket in order to keep + * the pool below a given maximum size limit. + */ +public class LruBitmapPool implements BitmapPool { + private static final String TAG = "LruBitmapPool"; + private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888; + + private final LruPoolStrategy strategy; + private final Set allowedConfigs; + private final int initialMaxSize; + private final BitmapTracker tracker; + + private int maxSize; + private int currentSize; + private int hits; + private int misses; + private int puts; + private int evictions; + + // Exposed for testing only. + LruBitmapPool(int maxSize, LruPoolStrategy strategy, Set allowedConfigs) { + this.initialMaxSize = maxSize; + this.maxSize = maxSize; + this.strategy = strategy; + this.allowedConfigs = allowedConfigs; + this.tracker = new NullBitmapTracker(); + } + + /** + * Constructor for LruBitmapPool. + * + * @param maxSize The initial maximum size of the pool in bytes. + */ + public LruBitmapPool(int maxSize) { + this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs()); + } + + /** + * Constructor for LruBitmapPool. + * + * @param maxSize The initial maximum size of the pool in bytes. + * @param allowedConfigs A white listed set of {@link Bitmap.Config} that are allowed to be put + * into the pool. Configs not in the allowed set will be rejected. + */ + public LruBitmapPool(int maxSize, Set allowedConfigs) { + this(maxSize, getDefaultStrategy(), allowedConfigs); + } + + @Override + public int getMaxSize() { + return maxSize; + } + + @Override + public synchronized void setSizeMultiplier(float sizeMultiplier) { + maxSize = Math.round(initialMaxSize * sizeMultiplier); + evict(); + } + + @Override + public synchronized boolean put(Bitmap bitmap) { + if (bitmap == null) { + throw new NullPointerException("Bitmap must not be null"); + } + if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Reject bitmap from pool" + + ", bitmap: " + strategy.logBitmap(bitmap) + + ", is mutable: " + bitmap.isMutable() + + ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig())); + } + return false; + } + + final int size = strategy.getSize(bitmap); + strategy.put(bitmap); + tracker.add(bitmap); + + puts++; + currentSize += size; + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap)); + } + dump(); + + evict(); + return true; + } + + private void evict() { + trimToSize(maxSize); + } + + @Override + public synchronized Bitmap get(int width, int height, Bitmap.Config config) { + Bitmap result = getDirty(width, height, config); + if (result != null) { + // Bitmaps in the pool contain random data that in some cases must be cleared for an image to be rendered + // correctly. we shouldn't force all consumers to independently erase the contents individually, so we do so + // here. See issue #131. + result.eraseColor(Color.TRANSPARENT); + } + + return result; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + @Override + public synchronized Bitmap getDirty(int width, int height, Bitmap.Config config) { + // Config will be null for non public config types, which can lead to transformations naively passing in + // null as the requested config here. See issue #194. + final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG); + if (result == null) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config)); + } + misses++; + } else { + hits++; + currentSize -= strategy.getSize(result); + tracker.remove(result); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { + result.setHasAlpha(true); + } + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config)); + } + dump(); + + return result; + } + + @Override + public void clearMemory() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "clearMemory"); + } + trimToSize(0); + } + + @SuppressLint("InlinedApi") + @Override + public void trimMemory(int level) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "trimMemory, level=" + level); + } + if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + clearMemory(); + } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + trimToSize(maxSize / 2); + } + } + + private synchronized void trimToSize(int size) { + while (currentSize > size) { + final Bitmap removed = strategy.removeLast(); + // TODO: This shouldn't ever happen, see #331. + if (removed == null) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Size mismatch, resetting"); + dumpUnchecked(); + } + currentSize = 0; + return; + } + + tracker.remove(removed); + currentSize -= strategy.getSize(removed); + removed.recycle(); + evictions++; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed)); + } + dump(); + } + } + + private void dump() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + dumpUnchecked(); + } + } + + private void dumpUnchecked() { + Log.v(TAG, "Hits=" + hits + + ", misses=" + misses + + ", puts=" + puts + + ", evictions=" + evictions + + ", currentSize=" + currentSize + + ", maxSize=" + maxSize + + "\nStrategy=" + strategy); + } + + private static LruPoolStrategy getDefaultStrategy() { + final LruPoolStrategy strategy; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + strategy = new SizeConfigStrategy(); + } else { + strategy = new AttributeStrategy(); + } + return strategy; + } + + private static Set getDefaultAllowedConfigs() { + Set configs = new HashSet(); + configs.addAll(Arrays.asList(Bitmap.Config.values())); + if (Build.VERSION.SDK_INT >= 19) { + configs.add(null); + } + return Collections.unmodifiableSet(configs); + } + + private interface BitmapTracker { + void add(Bitmap bitmap); + void remove(Bitmap bitmap); + } + + @SuppressWarnings("unused") + // Only used for debugging + private static class ThrowingBitmapTracker implements BitmapTracker { + private final Set bitmaps = Collections.synchronizedSet(new HashSet()); + + @Override + public void add(Bitmap bitmap) { + if (bitmaps.contains(bitmap)) { + throw new IllegalStateException("Can't add already added bitmap: " + bitmap + " [" + bitmap.getWidth() + + "x" + bitmap.getHeight() + "]"); + } + bitmaps.add(bitmap); + } + + @Override + public void remove(Bitmap bitmap) { + if (!bitmaps.contains(bitmap)) { + throw new IllegalStateException("Cannot remove bitmap not in tracker"); + } + bitmaps.remove(bitmap); + } + } + + private static class NullBitmapTracker implements BitmapTracker { + @Override + public void add(Bitmap bitmap) { + // Do nothing. + } + + @Override + public void remove(Bitmap bitmap) { + // Do nothing. + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruPoolStrategy.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruPoolStrategy.java new file mode 100755 index 0000000..ff512a0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/LruPoolStrategy.java @@ -0,0 +1,12 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.graphics.Bitmap; + +interface LruPoolStrategy { + void put(Bitmap bitmap); + Bitmap get(int width, int height, Bitmap.Config config); + Bitmap removeLast(); + String logBitmap(Bitmap bitmap); + String logBitmap(int width, int height, Bitmap.Config config); + int getSize(Bitmap bitmap); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/Poolable.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/Poolable.java new file mode 100755 index 0000000..5f8ed78 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/Poolable.java @@ -0,0 +1,5 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +interface Poolable { + void offer(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/PrettyPrintTreeMap.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/PrettyPrintTreeMap.java new file mode 100755 index 0000000..68410ec --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/PrettyPrintTreeMap.java @@ -0,0 +1,18 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import java.util.TreeMap; + +class PrettyPrintTreeMap extends TreeMap { + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("( "); + for (Entry entry : entrySet()) { + sb.append('{').append(entry.getKey()).append(':').append(entry.getValue()).append("}, "); + } + if (!isEmpty()) { + sb.replace(sb.length() - 2, sb.length(), ""); + } + return sb.append(" )").toString(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java new file mode 100755 index 0000000..6df4fa2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java @@ -0,0 +1,239 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; + + +import com.example.bumptech.glide.util.Util; + +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +/** + * Keys {@link Bitmap Bitmaps} using both {@link Bitmap#getAllocationByteCount()} and + * the {@link Bitmap.Config} returned from {@link Bitmap#getConfig()}. + * + *

+ * Using both the config and the byte size allows us to safely re-use a greater variety of + * {@link Bitmap Bitmaps}, which increases the hit rate of the pool and therefore the performance + * of applications. This class works around #301 by only allowing re-use of {@link Bitmap Bitmaps} + * with a matching number of bytes per pixel. + *

+ */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public class SizeConfigStrategy implements LruPoolStrategy { + private static final int MAX_SIZE_MULTIPLE = 8; + private static final Bitmap.Config[] ARGB_8888_IN_CONFIGS = new Bitmap.Config[] { + Bitmap.Config.ARGB_8888, + // The value returned by Bitmaps with the hidden Bitmap config. + null, + }; + // We probably could allow ARGB_4444 and RGB_565 to decode into each other, but ARGB_4444 is deprecated and we'd + // rather be safe. + private static final Bitmap.Config[] RGB_565_IN_CONFIGS = new Bitmap.Config[] { + Bitmap.Config.RGB_565 + }; + private static final Bitmap.Config[] ARGB_4444_IN_CONFIGS = new Bitmap.Config[] { + Bitmap.Config.ARGB_4444 + }; + private static final Bitmap.Config[] ALPHA_8_IN_CONFIGS = new Bitmap.Config[] { + Bitmap.Config.ALPHA_8 + }; + + private final KeyPool keyPool = new KeyPool(); + private final GroupedLinkedMap groupedMap = new GroupedLinkedMap(); + private final Map> sortedSizes = + new HashMap>(); + + @Override + public void put(Bitmap bitmap) { + int size = Util.getBitmapByteSize(bitmap); + Key key = keyPool.get(size, bitmap.getConfig()); + + groupedMap.put(key, bitmap); + + NavigableMap sizes = getSizesForConfig(bitmap.getConfig()); + Integer current = sizes.get(key.size); + sizes.put(key.size, current == null ? 1 : current + 1); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + int size = Util.getBitmapByteSize(width, height, config); + Key targetKey = keyPool.get(size, config); + Key bestKey = findBestKey(targetKey, size, config); + + Bitmap result = groupedMap.get(bestKey); + if (result != null) { + // Decrement must be called before reconfigure. + decrementBitmapOfSize(Util.getBitmapByteSize(result), result.getConfig()); + result.reconfigure(width, height, + result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888); + } + return result; + } + + private Key findBestKey(Key key, int size, Bitmap.Config config) { + Key result = key; + for (Bitmap.Config possibleConfig : getInConfigs(config)) { + NavigableMap sizesForPossibleConfig = getSizesForConfig(possibleConfig); + Integer possibleSize = sizesForPossibleConfig.ceilingKey(size); + if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) { + if (possibleSize != size + || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) { + keyPool.offer(key); + result = keyPool.get(possibleSize, possibleConfig); + } + break; + } + } + return result; + } + + @Override + public Bitmap removeLast() { + Bitmap removed = groupedMap.removeLast(); + if (removed != null) { + int removedSize = Util.getBitmapByteSize(removed); + decrementBitmapOfSize(removedSize, removed.getConfig()); + } + return removed; + } + + private void decrementBitmapOfSize(Integer size, Bitmap.Config config) { + NavigableMap sizes = getSizesForConfig(config); + Integer current = sizes.get(size); + if (current == 1) { + sizes.remove(size); + } else { + sizes.put(size, current - 1); + } + } + + private NavigableMap getSizesForConfig(Bitmap.Config config) { + NavigableMap sizes = sortedSizes.get(config); + if (sizes == null) { + sizes = new TreeMap(); + sortedSizes.put(config, sizes); + } + return sizes; + } + + @Override + public String logBitmap(Bitmap bitmap) { + int size = Util.getBitmapByteSize(bitmap); + return getBitmapString(size, bitmap.getConfig()); + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + int size = Util.getBitmapByteSize(width, height, config); + return getBitmapString(size, config); + } + + @Override + public int getSize(Bitmap bitmap) { + return Util.getBitmapByteSize(bitmap); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append("SizeConfigStrategy{groupedMap=") + .append(groupedMap) + .append(", sortedSizes=("); + for (Map.Entry> entry : sortedSizes.entrySet()) { + sb.append(entry.getKey()).append('[').append(entry.getValue()).append("], "); + } + if (!sortedSizes.isEmpty()) { + sb.replace(sb.length() - 2, sb.length(), ""); + } + return sb.append(")}").toString(); + } + + // Visible for testing. + static class KeyPool extends BaseKeyPool { + + public Key get(int size, Bitmap.Config config) { + Key result = get(); + result.init(size, config); + return result; + } + + @Override + protected Key create() { + return new Key(this); + } + } + + // Visible for testing. + static final class Key implements Poolable { + private final KeyPool pool; + + private int size; + private Bitmap.Config config; + + public Key(KeyPool pool) { + this.pool = pool; + } + + // Visible for testing. + Key(KeyPool pool, int size, Bitmap.Config config) { + this(pool); + init(size, config); + } + + public void init(int size, Bitmap.Config config) { + this.size = size; + this.config = config; + } + + @Override + public void offer() { + pool.offer(this); + } + + @Override + public String toString() { + return getBitmapString(size, config); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key other = (Key) o; + return size == other.size && (config == null ? other.config == null : config.equals(other.config)); + } + return false; + } + + @Override + public int hashCode() { + int result = size; + result = 31 * result + (config != null ? config.hashCode() : 0); + return result; + } + } + + private static String getBitmapString(int size, Bitmap.Config config) { + return "[" + size + "](" + config + ")"; + } + + private static Bitmap.Config[] getInConfigs(Bitmap.Config requested) { + switch (requested) { + case ARGB_8888: + return ARGB_8888_IN_CONFIGS; + case RGB_565: + return RGB_565_IN_CONFIGS; + case ARGB_4444: + return ARGB_4444_IN_CONFIGS; + case ALPHA_8: + return ALPHA_8_IN_CONFIGS; + default: + return new Bitmap.Config[] { requested }; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeStrategy.java b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeStrategy.java new file mode 100755 index 0000000..8eb0487 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/bitmap_recycle/SizeStrategy.java @@ -0,0 +1,158 @@ +package com.example.bumptech.glide.load.engine.bitmap_recycle; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; + + +import com.example.bumptech.glide.util.Util; + +import java.util.TreeMap; + +/** + * A strategy for reusing bitmaps that relies on {@link Bitmap#reconfigure(int, int, Bitmap.Config)}. + * Requires {@link Build.VERSION_CODES#KITKAT KitKat} (API {@value Build.VERSION_CODES#KITKAT}) or higher. + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +class SizeStrategy implements LruPoolStrategy { + private static final int MAX_SIZE_MULTIPLE = 8; + private final KeyPool keyPool = new KeyPool(); + private final GroupedLinkedMap groupedMap = new GroupedLinkedMap(); + private final TreeMap sortedSizes = new PrettyPrintTreeMap(); + + @Override + public void put(Bitmap bitmap) { + int size = Util.getBitmapByteSize(bitmap); + final Key key = keyPool.get(size); + + groupedMap.put(key, bitmap); + + Integer current = sortedSizes.get(key.size); + sortedSizes.put(key.size, current == null ? 1 : current + 1); + } + + @Override + public Bitmap get(int width, int height, Bitmap.Config config) { + final int size = Util.getBitmapByteSize(width, height, config); + Key key = keyPool.get(size); + + Integer possibleSize = sortedSizes.ceilingKey(size); + if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) { + keyPool.offer(key); + key = keyPool.get(possibleSize); + } + + // Do a get even if we know we don't have a bitmap so that the key moves to the front in the lru pool + final Bitmap result = groupedMap.get(key); + if (result != null) { + result.reconfigure(width, height, config); + decrementBitmapOfSize(possibleSize); + } + + return result; + } + + @Override + public Bitmap removeLast() { + Bitmap removed = groupedMap.removeLast(); + if (removed != null) { + final int removedSize = Util.getBitmapByteSize(removed); + decrementBitmapOfSize(removedSize); + } + return removed; + } + + private void decrementBitmapOfSize(Integer size) { + Integer current = sortedSizes.get(size); + if (current == 1) { + sortedSizes.remove(size); + } else { + sortedSizes.put(size, current - 1); + } + } + + @Override + public String logBitmap(Bitmap bitmap) { + return getBitmapString(bitmap); + } + + @Override + public String logBitmap(int width, int height, Bitmap.Config config) { + int size = Util.getBitmapByteSize(width, height, config); + return getBitmapString(size); + } + + @Override + public int getSize(Bitmap bitmap) { + return Util.getBitmapByteSize(bitmap); + } + + @Override + public String toString() { + return "SizeStrategy:\n " + + groupedMap + "\n" + + " SortedSizes" + sortedSizes; + } + + private static String getBitmapString(Bitmap bitmap) { + int size = Util.getBitmapByteSize(bitmap); + return getBitmapString(size); + } + + private static String getBitmapString(int size) { + return "[" + size + "]"; + } + + // Visible for testing. + static class KeyPool extends BaseKeyPool { + + public Key get(int size) { + Key result = get(); + result.init(size); + return result; + } + + @Override + protected Key create() { + return new Key(this); + } + } + + // Visible for testing. + static final class Key implements Poolable { + private final KeyPool pool; + private int size; + + Key(KeyPool pool) { + this.pool = pool; + } + + public void init(int size) { + this.size = size; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Key) { + Key other = (Key) o; + return size == other.size; + } + return false; + } + + @Override + public int hashCode() { + return size; + } + + @Override + public String toString() { + return getBitmapString(size); + } + + @Override + public void offer() { + pool.offer(this); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCache.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCache.java new file mode 100755 index 0000000..993835b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCache.java @@ -0,0 +1,76 @@ +package com.example.bumptech.glide.load.engine.cache; + + +import com.example.bumptech.glide.load.Key; + +import java.io.File; + +/** + * An interface for writing to and reading from a disk cache. + */ +public interface DiskCache { + + /** + * An interface for lazily creating a disk cache. + */ + interface Factory { + + /** 250 MB of cache. */ + int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024; + String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache"; + + /** + * Returns a new disk cache, or {@code null} if no disk cache could be created. + */ + DiskCache build(); + + File getCacheDir(); + } + + /** + * An interface to actually write data to a key in the disk cache. + */ + interface Writer { + /** + * Writes data to the file and returns true if the write was successful and should be committed, and + * false if the write should be aborted. + * + * @param file The File the Writer should write to. + */ + boolean write(File file); + } + + /** + * Get the cache for the value at the given key. + * + *

+ * Note - This is potentially dangerous, someone may write a new value to the file at any point in timeand we + * won't know about it. + *

+ * + * @param key The key in the cache. + * @return An InputStream representing the data at key at the time get is called. + */ + File get(Key key); + + /** + * Write to a key in the cache. {@link Writer} is used so that the cache implementation can perform actions after + * the write finishes, like commit (via atomic file rename). + * + * @param key The key to write to. + * @param writer An interface that will write data given an OutputStream for the key. + */ + void put(Key key, Writer writer); + + /** + * Remove the key and value from the cache. + * + * @param key The key to remove. + */ + void delete(Key key); + + /** + * Clear the cache. + */ + void clear(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheAdapter.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheAdapter.java new file mode 100755 index 0000000..70c4b55 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheAdapter.java @@ -0,0 +1,32 @@ +package com.example.bumptech.glide.load.engine.cache; + + +import com.example.bumptech.glide.load.Key; + +import java.io.File; + +/** + * A simple class that returns null for all gets and ignores all writes. + */ +public class DiskCacheAdapter implements DiskCache { + @Override + public File get(Key key) { + // no op, default for overriders + return null; + } + + @Override + public void put(Key key, Writer writer) { + // no op, default for overriders + } + + @Override + public void delete(Key key) { + // no op, default for overriders + } + + @Override + public void clear() { + // no op, default for overriders + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheWriteLocker.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheWriteLocker.java new file mode 100755 index 0000000..eed6324 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskCacheWriteLocker.java @@ -0,0 +1,91 @@ +package com.example.bumptech.glide.load.engine.cache; + +import com.example.bumptech.glide.load.Key; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Keeps a map of keys to locks that allows locks to be removed from the map when no longer in use + * so the size of the collection is bounded. + * + *

This class will be accessed by multiple threads in a thread pool and ensures that the + * number of threads interested in each lock is updated atomically so that when the count reaches + * 0, the lock can safely be removed from the map.

+ */ +final class DiskCacheWriteLocker { + private final Map locks = new HashMap(); + private final WriteLockPool writeLockPool = new WriteLockPool(); + + void acquire(Key key) { + WriteLock writeLock; + synchronized (this) { + writeLock = locks.get(key); + if (writeLock == null) { + writeLock = writeLockPool.obtain(); + locks.put(key, writeLock); + } + writeLock.interestedThreads++; + } + + writeLock.lock.lock(); + } + + void release(Key key) { + WriteLock writeLock; + synchronized (this) { + writeLock = locks.get(key); + if (writeLock == null || writeLock.interestedThreads <= 0) { + throw new IllegalArgumentException( + "Cannot release a lock that is not held" + ", key: " + key + ", interestedThreads: " + + (writeLock == null ? 0 : writeLock.interestedThreads)); + } + + if (--writeLock.interestedThreads == 0) { + WriteLock removed = locks.remove(key); + if (!removed.equals(writeLock)) { + throw new IllegalStateException("Removed the wrong lock" + + ", expected to remove: " + writeLock + + ", but actually removed: " + removed + + ", key: " + key); + } + writeLockPool.offer(removed); + } + } + + writeLock.lock.unlock(); + } + + private static class WriteLock { + final Lock lock = new ReentrantLock(); + int interestedThreads; + } + + private static class WriteLockPool { + private static final int MAX_POOL_SIZE = 10; + private final Queue pool = new ArrayDeque(); + + WriteLock obtain() { + WriteLock result; + synchronized (pool) { + result = pool.poll(); + } + if (result == null) { + result = new WriteLock(); + } + return result; + } + + void offer(WriteLock writeLock) { + synchronized (pool) { + if (pool.size() < MAX_POOL_SIZE) { + pool.offer(writeLock); + } + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheFactory.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheFactory.java new file mode 100755 index 0000000..6b0dffd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheFactory.java @@ -0,0 +1,74 @@ +package com.example.bumptech.glide.load.engine.cache; + +import java.io.File; + +/** + * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the specified disk cache + * directory. + *

+ * If you need to make I/O access before returning the cache directory use + * the {@link DiskLruCacheFactory#DiskLruCacheFactory(CacheDirectoryGetter, int)} constructor variant. + */ +public class DiskLruCacheFactory implements DiskCache.Factory { + + private final int diskCacheSize; + private final CacheDirectoryGetter cacheDirectoryGetter; + + /** + * Interface called out of UI thread to get the cache folder. + */ + public interface CacheDirectoryGetter { + File getCacheDirectory(); + } + + public DiskLruCacheFactory(final String diskCacheFolder, int diskCacheSize) { + this(new CacheDirectoryGetter() { + @Override + public File getCacheDirectory() { + return new File(diskCacheFolder); + } + }, diskCacheSize); + } + + public DiskLruCacheFactory(final String diskCacheFolder, final String diskCacheName, int diskCacheSize) { + this(new CacheDirectoryGetter() { + @Override + public File getCacheDirectory() { + return new File(diskCacheFolder, diskCacheName); + } + }, diskCacheSize); + } + + /** + * When using this constructor {@link CacheDirectoryGetter#getCacheDirectory()} will be called out of UI thread, + * allowing to do I/O access without performance impacts. + * + * @param cacheDirectoryGetter Interface called out of UI thread to get the cache folder. + * @param diskCacheSize Desired max bytes size for the LRU disk cache. + */ + public DiskLruCacheFactory(CacheDirectoryGetter cacheDirectoryGetter, int diskCacheSize) { + this.diskCacheSize = diskCacheSize; + this.cacheDirectoryGetter = cacheDirectoryGetter; + } + + @Override + public DiskCache build() { + File cacheDir = cacheDirectoryGetter.getCacheDirectory(); + + if (cacheDir == null) { + return null; + } + + if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) { + return null; + } + + return DiskLruCacheWrapper.get(cacheDir, diskCacheSize); + } + + @Override + public File getCacheDir() { + return cacheDirectoryGetter.getCacheDirectory(); + } + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheWrapper.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheWrapper.java new file mode 100755 index 0000000..da9e52b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/DiskLruCacheWrapper.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013. Bump Technologies Inc. All Rights Reserved. + */ + +package com.example.bumptech.glide.load.engine.cache; + +import android.util.Log; + + +import com.example.bumptech.glide.disklrucache.DiskLruCache; +import com.example.bumptech.glide.load.Key; + +import java.io.File; +import java.io.IOException; + +/** + * The default DiskCache implementation. There must be no more than one active instance for a given + * directory at a time. + * + * @see #get(File, int) + */ +public class DiskLruCacheWrapper implements DiskCache { + private static final String TAG = "DiskLruCacheWrapper"; + + private static final int APP_VERSION = 1; + private static final int VALUE_COUNT = 1; + private static DiskLruCacheWrapper wrapper = null; + + private final DiskCacheWriteLocker writeLocker = new DiskCacheWriteLocker(); + private final SafeKeyGenerator safeKeyGenerator; + private final File directory; + private final int maxSize; + private DiskLruCache diskLruCache; + + /** + * Get a DiskCache in the given directory and size. If a disk cache has alread been created with + * a different directory and/or size, it will be returned instead and the new arguments + * will be ignored. + * + * @param directory The directory for the disk cache + * @param maxSize The max size for the disk cache + * @return The new disk cache with the given arguments, or the current cache if one already exists + */ + public static synchronized DiskCache get(File directory, int maxSize) { + // TODO calling twice with different arguments makes it return the cache for the same directory, it's public! + if (wrapper == null) { + wrapper = new DiskLruCacheWrapper(directory, maxSize); + } + return wrapper; + } + + protected DiskLruCacheWrapper(File directory, int maxSize) { + this.directory = directory; + this.maxSize = maxSize; + this.safeKeyGenerator = SafeKeyGenerator.getInstance(); + } + + public synchronized DiskLruCache getDiskCache() throws IOException { + if (diskLruCache == null) { + diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize); + } + return diskLruCache; + } + + private synchronized void resetDiskCache() { + diskLruCache = null; + } + + @Override + public File get(Key key) { + String safeKey = safeKeyGenerator.getSafeKey(key); + File result = null; + try { + //It is possible that the there will be a put in between these two gets. If so that shouldn't be a problem + //because we will always put the same value at the same key so our input streams will still represent + //the same data + final DiskLruCache.Value value = getDiskCache().get(safeKey); + if (value != null) { + result = value.getFile(0); + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unable to get from disk cache", e); + } + } + return result; + } + + @Override + public void put(Key key, Writer writer) { + String safeKey = safeKeyGenerator.getSafeKey(key); + writeLocker.acquire(key); + try { + DiskLruCache.Editor editor = getDiskCache().edit(safeKey); + // Editor will be null if there are two concurrent puts. In the worst case we will just silently fail. + if (editor != null) { + try { + File file = editor.getFile(0); + if (writer.write(file)) { + editor.commit(); + } + } finally { + editor.abortUnlessCommitted(); + } + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unable to put to disk cache", e); + } + } finally { + writeLocker.release(key); + } + } + + @Override + public void delete(Key key) { + String safeKey = safeKeyGenerator.getSafeKey(key); + try { + getDiskCache().remove(safeKey); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unable to delete from disk cache", e); + } + } + } + + @Override + public synchronized void clear() { + try { + getDiskCache().delete(); + resetDiskCache(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unable to clear disk cache", e); + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/ExternalCacheDiskCacheFactory.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/ExternalCacheDiskCacheFactory.java new file mode 100755 index 0000000..8b5e900 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/ExternalCacheDiskCacheFactory.java @@ -0,0 +1,38 @@ +package com.example.bumptech.glide.load.engine.cache; + +import android.content.Context; + +import java.io.File; + +/** + * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the external disk cache + * directory. + *

+ * Images can be read by everyone when using external disk cache. + */ +public final class ExternalCacheDiskCacheFactory extends DiskLruCacheFactory { + + public ExternalCacheDiskCacheFactory(Context context) { + this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); + } + + public ExternalCacheDiskCacheFactory(Context context, int diskCacheSize) { + this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); + } + + public ExternalCacheDiskCacheFactory(final Context context, final String diskCacheName, int diskCacheSize) { + super(new CacheDirectoryGetter() { + @Override + public File getCacheDirectory() { + File cacheDirectory = context.getExternalCacheDir(); + if (cacheDirectory == null) { + return null; + } + if (diskCacheName != null) { + return new File(cacheDirectory, diskCacheName); + } + return cacheDirectory; + } + }, diskCacheSize); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/InternalCacheDiskCacheFactory.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/InternalCacheDiskCacheFactory.java new file mode 100755 index 0000000..0ee55b3 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/InternalCacheDiskCacheFactory.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.engine.cache; + +import android.content.Context; + +import java.io.File; + +/** + * Creates an {@link com.bumptech.glide.disklrucache.DiskLruCache} based disk cache in the internal disk cache + * directory. + */ +public final class InternalCacheDiskCacheFactory extends DiskLruCacheFactory { + + public InternalCacheDiskCacheFactory(Context context) { + this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE); + } + + public InternalCacheDiskCacheFactory(Context context, int diskCacheSize) { + this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize); + } + + public InternalCacheDiskCacheFactory(final Context context, final String diskCacheName, int diskCacheSize) { + super(new CacheDirectoryGetter() { + @Override + public File getCacheDirectory() { + File cacheDirectory = context.getCacheDir(); + if (cacheDirectory == null) { + return null; + } + if (diskCacheName != null) { + return new File(cacheDirectory, diskCacheName); + } + return cacheDirectory; + } + }, diskCacheSize); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/LruResourceCache.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/LruResourceCache.java new file mode 100755 index 0000000..23b99ad --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/LruResourceCache.java @@ -0,0 +1,55 @@ +package com.example.bumptech.glide.load.engine.cache; + +import android.annotation.SuppressLint; + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.util.LruCache; + + +/** + * An LRU in memory cache for {@link Resource}s. + */ +public class LruResourceCache extends LruCache> implements MemoryCache { + private ResourceRemovedListener listener; + + /** + * Constructor for LruResourceCache. + * + * @param size The maximum size in bytes the in memory cache can use. + */ + public LruResourceCache(int size) { + super(size); + } + + @Override + public void setResourceRemovedListener(ResourceRemovedListener listener) { + this.listener = listener; + } + + @Override + protected void onItemEvicted(Key key, Resource item) { + if (listener != null) { + listener.onResourceRemoved(item); + } + } + + @Override + protected int getSize(Resource item) { + return item.getSize(); + } + + @SuppressLint("InlinedApi") + @Override + public void trimMemory(int level) { + if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) { + // Nearing middle of list of cached background apps + // Evict our entire bitmap cache + clearMemory(); + } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + // Entering list of cached background apps + // Evict oldest half of our bitmap cache + trimToSize(getCurrentSize() / 2); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCache.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCache.java new file mode 100755 index 0000000..a5a148b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCache.java @@ -0,0 +1,74 @@ +package com.example.bumptech.glide.load.engine.cache; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.engine.Resource; + +/** + * An interface for adding and removing resources from an in memory cache. + */ +public interface MemoryCache { + /** + * An interface that will be called whenever a bitmap is removed from the cache. + */ + interface ResourceRemovedListener { + void onResourceRemoved(Resource removed); + } + + /** + * Returns the sum of the sizes of all the contents of the cache in bytes. + */ + int getCurrentSize(); + + /** + * Returns the current maximum size in bytes of the cache. + */ + int getMaxSize(); + + /** + * Adjust the maximum size of the cache by multiplying the original size of the cache by the given multiplier. + * + *

+ * If the size multiplier causes the size of the cache to be decreased, items will be evicted until the cache + * is smaller than the new size. + *

+ * + * @param multiplier A size multiplier >= 0. + */ + void setSizeMultiplier(float multiplier); + + /** + * Removes the value for the given key and returns it if present or null otherwise. + * + * @param key The key. + */ + Resource remove(Key key); + + /** + * Add bitmap to the cache with the given key. + * + * @param key The key to retrieve the bitmap. + * @param resource The {@link com.bumptech.glide.load.engine.EngineResource} to store. + * @return The old value of key (null if key is not in map). + */ + Resource put(Key key, Resource resource); + + /** + * Set the listener to be called when a bitmap is removed from the cache. + * + * @param listener The listener. + */ + void setResourceRemovedListener(ResourceRemovedListener listener); + + /** + * Evict all items from the memory cache. + */ + void clearMemory(); + + /** + * Trim the memory cache to the appropriate level. Typically called on the callback onTrimMemory. + * + * @param level This integer represents a trim level as specified in {@link android.content.ComponentCallbacks2}. + */ + void trimMemory(int level); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCacheAdapter.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCacheAdapter.java new file mode 100755 index 0000000..d83483c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemoryCacheAdapter.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.engine.cache; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A simple class that ignores all puts and returns null for all gets. + */ +public class MemoryCacheAdapter implements MemoryCache { + + private ResourceRemovedListener listener; + + @Override + public int getCurrentSize() { + return 0; + } + + @Override + public int getMaxSize() { + return 0; + } + + @Override + public void setSizeMultiplier(float multiplier) { + // Do nothing. + } + + @Override + public Resource remove(Key key) { + return null; + } + + @Override + public Resource put(Key key, Resource resource) { + listener.onResourceRemoved(resource); + return null; + } + + @Override + public void setResourceRemovedListener(ResourceRemovedListener listener) { + this.listener = listener; + } + + @Override + public void clearMemory() { + // Do nothing. + } + + @Override + public void trimMemory(int level) { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemorySizeCalculator.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemorySizeCalculator.java new file mode 100755 index 0000000..f9d5ed0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/MemorySizeCalculator.java @@ -0,0 +1,119 @@ +package com.example.bumptech.glide.load.engine.cache; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.Context; +import android.os.Build; +import android.text.format.Formatter; +import android.util.DisplayMetrics; +import android.util.Log; + +/** + * A calculator that tries to intelligently determine cache sizes for a given device based on some constants and the + * devices screen density, width, and height. + */ +public class MemorySizeCalculator { + private static final String TAG = "MemorySizeCalculator"; + + // Visible for testing. + static final int BYTES_PER_ARGB_8888_PIXEL = 4; + static final int MEMORY_CACHE_TARGET_SCREENS = 2; + static final int BITMAP_POOL_TARGET_SCREENS = 4; + static final float MAX_SIZE_MULTIPLIER = 0.4f; + static final float LOW_MEMORY_MAX_SIZE_MULTIPLIER = 0.33f; + + private final int bitmapPoolSize; + private final int memoryCacheSize; + private final Context context; + + interface ScreenDimensions { + int getWidthPixels(); + int getHeightPixels(); + } + + public MemorySizeCalculator(Context context) { + this(context, + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE), + new DisplayMetricsScreenDimensions(context.getResources().getDisplayMetrics())); + } + + // Visible for testing. + MemorySizeCalculator(Context context, ActivityManager activityManager, ScreenDimensions screenDimensions) { + this.context = context; + final int maxSize = getMaxSize(activityManager); + + final int screenSize = screenDimensions.getWidthPixels() * screenDimensions.getHeightPixels() + * BYTES_PER_ARGB_8888_PIXEL; + + int targetPoolSize = screenSize * BITMAP_POOL_TARGET_SCREENS; + int targetMemoryCacheSize = screenSize * MEMORY_CACHE_TARGET_SCREENS; + + if (targetMemoryCacheSize + targetPoolSize <= maxSize) { + memoryCacheSize = targetMemoryCacheSize; + bitmapPoolSize = targetPoolSize; + } else { + int part = Math.round((float) maxSize / (BITMAP_POOL_TARGET_SCREENS + MEMORY_CACHE_TARGET_SCREENS)); + memoryCacheSize = part * MEMORY_CACHE_TARGET_SCREENS; + bitmapPoolSize = part * BITMAP_POOL_TARGET_SCREENS; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Calculated memory cache size: " + toMb(memoryCacheSize) + " pool size: " + toMb(bitmapPoolSize) + + " memory class limited? " + (targetMemoryCacheSize + targetPoolSize > maxSize) + " max size: " + + toMb(maxSize) + " memoryClass: " + activityManager.getMemoryClass() + " isLowMemoryDevice: " + + isLowMemoryDevice(activityManager)); + } + } + + /** + * Returns the recommended memory cache size for the device it is run on in bytes. + */ + public int getMemoryCacheSize() { + return memoryCacheSize; + } + + /** + * Returns the recommended bitmap pool size for the device it is run on in bytes. + */ + public int getBitmapPoolSize() { + return bitmapPoolSize; + } + + private static int getMaxSize(ActivityManager activityManager) { + final int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024; + final boolean isLowMemoryDevice = isLowMemoryDevice(activityManager); + return Math.round(memoryClassBytes + * (isLowMemoryDevice ? LOW_MEMORY_MAX_SIZE_MULTIPLIER : MAX_SIZE_MULTIPLIER)); + } + + private String toMb(int bytes) { + return Formatter.formatFileSize(context, bytes); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static boolean isLowMemoryDevice(ActivityManager activityManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return activityManager.isLowRamDevice(); + } else { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB; + } + } + + private static class DisplayMetricsScreenDimensions implements ScreenDimensions { + private final DisplayMetrics displayMetrics; + + public DisplayMetricsScreenDimensions(DisplayMetrics displayMetrics) { + this.displayMetrics = displayMetrics; + } + + @Override + public int getWidthPixels() { + return displayMetrics.widthPixels; + } + + @Override + public int getHeightPixels() { + return displayMetrics.heightPixels; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/cache/SafeKeyGenerator.java b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/SafeKeyGenerator.java new file mode 100755 index 0000000..ded2ade --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/cache/SafeKeyGenerator.java @@ -0,0 +1,52 @@ +package com.example.bumptech.glide.load.engine.cache; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.util.LruCache; +import com.example.bumptech.glide.util.Util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * A class that generates and caches safe and unique string file names from {@link Key}s. + */ +public class SafeKeyGenerator { + private final LruCache loadIdToSafeHash = new LruCache(1000); + + private static SafeKeyGenerator safeKeyGenerator; + + private SafeKeyGenerator() { + + } + + public synchronized static SafeKeyGenerator getInstance() { + if (safeKeyGenerator == null) { + safeKeyGenerator = new SafeKeyGenerator(); + } + return safeKeyGenerator; + } + + public String getSafeKey(Key key) { + String safeKey; + synchronized (loadIdToSafeHash) { + safeKey = loadIdToSafeHash.get(key); + } + if (safeKey == null) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + key.updateDiskCacheKey(messageDigest); + safeKey = Util.sha256BytesToHex(messageDigest.digest()); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + synchronized (loadIdToSafeHash) { + loadIdToSafeHash.put(key, safeKey); + } + } + return safeKey; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/executor/FifoPriorityThreadPoolExecutor.java b/core/src/main/java/com/example/bumptech/glide/load/engine/executor/FifoPriorityThreadPoolExecutor.java new file mode 100755 index 0000000..d1f15f3 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/executor/FifoPriorityThreadPoolExecutor.java @@ -0,0 +1,167 @@ +package com.example.bumptech.glide.load.engine.executor; + +import android.util.Log; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A FIFO priority {@link ThreadPoolExecutor} that prioritizes submitted {@link Runnable}s by assuming they implement + * {@link Prioritized}. {@link Prioritized} runnables that return lower values for {@link Prioritized#getPriority()} + * will be executed before those that return higher values. Priorities only apply when multiple items are queued at the + * same time. Runnables with the same priority will be executed in FIFO order. + */ +public class FifoPriorityThreadPoolExecutor extends ThreadPoolExecutor { + private static final String TAG = "PriorityExecutor"; + private final AtomicInteger ordering = new AtomicInteger(); + private final UncaughtThrowableStrategy uncaughtThrowableStrategy; + + /** + * A strategy for handling unexpected and uncaught throwables thrown by futures run on the pool. + */ + public enum UncaughtThrowableStrategy { + /** Silently catches and ignores the uncaught throwables. */ + IGNORE, + /** Logs the uncaught throwables using {@link #TAG} and {@link Log}. */ + LOG { + @Override + protected void handle(Throwable t) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "Request threw uncaught throwable", t); + } + } + }, + /** Rethrows the uncaught throwables to crash the app. */ + THROW { + @Override + protected void handle(Throwable t) { + super.handle(t); + throw new RuntimeException(t); + } + }; + + protected void handle(Throwable t) { + // Ignore. + } + } + + /** + * Constructor to build a fixed thread pool with the given pool size using + * {@link DefaultThreadFactory}. + * + * @param poolSize The number of threads. + */ + public FifoPriorityThreadPoolExecutor(int poolSize) { + this(poolSize, UncaughtThrowableStrategy.LOG); + } + + /** + * Constructor to build a fixed thread pool with the given pool size using + * {@link DefaultThreadFactory}. + * + * @param poolSize The number of threads. + * @param uncaughtThrowableStrategy Dictates how the pool should handle uncaught and unexpected throwables + * thrown by Futures run by the pool. + */ + public FifoPriorityThreadPoolExecutor(int poolSize, UncaughtThrowableStrategy uncaughtThrowableStrategy) { + this(poolSize, poolSize, 0, TimeUnit.MILLISECONDS, new DefaultThreadFactory(), + uncaughtThrowableStrategy); + } + + public FifoPriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAlive, TimeUnit timeUnit, + ThreadFactory threadFactory, UncaughtThrowableStrategy uncaughtThrowableStrategy) { + super(corePoolSize, maximumPoolSize, keepAlive, timeUnit, new PriorityBlockingQueue(), threadFactory); + this.uncaughtThrowableStrategy = uncaughtThrowableStrategy; + } + + @Override + protected RunnableFuture newTaskFor(Runnable runnable, T value) { + return new LoadTask(runnable, value, ordering.getAndIncrement()); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + Future future = (Future) r; + if (future.isDone() && !future.isCancelled()) { + try { + future.get(); + } catch (InterruptedException e) { + uncaughtThrowableStrategy.handle(e); + } catch (ExecutionException e) { + uncaughtThrowableStrategy.handle(e); + } + } + } + } + + /** + * A {@link ThreadFactory} that builds threads with priority + * {@link android.os.Process#THREAD_PRIORITY_BACKGROUND}. + */ + public static class DefaultThreadFactory implements ThreadFactory { + int threadNum = 0; + @Override + public Thread newThread(Runnable runnable) { + final Thread result = new Thread(runnable, "fifo-pool-thread-" + threadNum) { + @Override + public void run() { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + super.run(); + } + }; + threadNum++; + return result; + } + } + + // Visible for testing. + static class LoadTask extends FutureTask implements Comparable> { + private final int priority; + private final int order; + + public LoadTask(Runnable runnable, T result, int order) { + super(runnable, result); + if (!(runnable instanceof Prioritized)) { + throw new IllegalArgumentException("FifoPriorityThreadPoolExecutor must be given Runnables that " + + "implement Prioritized"); + } + priority = ((Prioritized) runnable).getPriority(); + this.order = order; + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object o) { + if (o instanceof LoadTask) { + LoadTask other = (LoadTask) o; + return order == other.order && priority == other.priority; + } + return false; + } + + @Override + public int hashCode() { + int result = priority; + result = 31 * result + order; + return result; + } + + @Override + public int compareTo(LoadTask loadTask) { + int result = priority - loadTask.priority; + if (result == 0) { + result = order - loadTask.order; + } + return result; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/executor/Prioritized.java b/core/src/main/java/com/example/bumptech/glide/load/engine/executor/Prioritized.java new file mode 100755 index 0000000..edcdde2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/executor/Prioritized.java @@ -0,0 +1,12 @@ +package com.example.bumptech.glide.load.engine.executor; + +/** + * A simple interface for exposing the priority of a task. Lower integer values are treated as having higher priority + * with 0 being the highest priority possible. + */ +public interface Prioritized { + /** + * Returns the priority of this task. + */ + int getPriority(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java new file mode 100755 index 0000000..f3688b5 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFillRunner.java @@ -0,0 +1,162 @@ +package com.example.bumptech.glide.load.engine.prefill; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + + +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.engine.cache.MemoryCache; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; +import com.example.bumptech.glide.util.Util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * A class that allocates {@link Bitmap Bitmaps} to make sure that the + * {@link BitmapPool} is pre-populated. + * + *

By posting to the main thread with backoffs, we try to avoid ANRs when the garbage collector gets into a state + * where a high percentage of {@link Bitmap} allocations trigger a stop the world GC. We try to detect whether or not a + * GC has occurred by only allowing our allocator to run for a limited number of milliseconds. Since the allocations + * themselves very fast, a GC is the most likely reason for a substantial delay. If we detect our allocator has run for + * more than our limit, we assume a GC has occurred, stop the current allocations, and try again after a delay. + */ +final class BitmapPreFillRunner implements Runnable { + private static final String TAG = "PreFillRunner"; + private static final Clock DEFAULT_CLOCK = new Clock(); + + /** + * The maximum number of millis we can run before posting. Set to match and detect the duration of non concurrent + * GCs. + */ + static final long MAX_DURATION_MS = 32; + + /** + * The amount of time in ms we wait before continuing to allocate after the first GC is detected. + */ + static final long INITIAL_BACKOFF_MS = 40; + + /** + * The amount by which the current backoff time is multiplied each time we detect a GC. + */ + static final int BACKOFF_RATIO = 4; + + /** + * The maximum amount of time in ms we wait before continuing to allocate. + */ + static final long MAX_BACKOFF_MS = TimeUnit.SECONDS.toMillis(1); + + private final BitmapPool bitmapPool; + private final MemoryCache memoryCache; + private final PreFillQueue toPrefill; + private final Clock clock; + private final Set seenTypes = new HashSet(); + private final Handler handler; + + private long currentDelay = INITIAL_BACKOFF_MS; + private boolean isCancelled; + + public BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder) { + this(bitmapPool, memoryCache, allocationOrder, DEFAULT_CLOCK, new Handler(Looper.getMainLooper())); + } + + // Visible for testing. + BitmapPreFillRunner(BitmapPool bitmapPool, MemoryCache memoryCache, PreFillQueue allocationOrder, Clock clock, + Handler handler) { + this.bitmapPool = bitmapPool; + this.memoryCache = memoryCache; + this.toPrefill = allocationOrder; + this.clock = clock; + this.handler = handler; + } + + public void cancel() { + isCancelled = true; + } + + /** + * Attempts to allocate {@link Bitmap}s and returns {@code true} if there are more + * {@link Bitmap}s to allocate and {@code false} otherwise. + */ + private boolean allocate() { + long start = clock.now(); + while (!toPrefill.isEmpty() && !isGcDetected(start)) { + PreFillType toAllocate = toPrefill.remove(); + Bitmap bitmap = Bitmap.createBitmap(toAllocate.getWidth(), toAllocate.getHeight(), + toAllocate.getConfig()); + + // Don't over fill the memory cache to avoid evicting useful resources, but make sure it's not empty so + // we use all available space. + if (getFreeMemoryCacheBytes() >= Util.getBitmapByteSize(bitmap)) { + memoryCache.put(new UniqueKey(), BitmapResource.obtain(bitmap, bitmapPool)); + } else { + addToBitmapPool(toAllocate, bitmap); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "allocated [" + toAllocate.getWidth() + "x" + toAllocate.getHeight() + "] " + + toAllocate.getConfig() + " size: " + Util.getBitmapByteSize(bitmap)); + } + } + + return !isCancelled && !toPrefill.isEmpty(); + } + + private boolean isGcDetected(long startTimeMs) { + return clock.now() - startTimeMs >= MAX_DURATION_MS; + } + + private int getFreeMemoryCacheBytes() { + return memoryCache.getMaxSize() - memoryCache.getCurrentSize(); + } + + private void addToBitmapPool(PreFillType toAllocate, Bitmap bitmap) { + // The pool may not move sizes to the front of the LRU on put. Do a get here to make sure the size we're adding + // is at the front of the queue so that the Bitmap we're adding won't be evicted immediately. + if (seenTypes.add(toAllocate)) { + Bitmap fromPool = bitmapPool.get(toAllocate.getWidth(), toAllocate.getHeight(), + toAllocate.getConfig()); + if (fromPool != null) { + bitmapPool.put(fromPool); + } + } + + bitmapPool.put(bitmap); + } + + @Override + public void run() { + if (allocate()) { + handler.postDelayed(this, getNextDelay()); + } + } + + private long getNextDelay() { + long result = currentDelay; + currentDelay = Math.min(currentDelay * BACKOFF_RATIO, MAX_BACKOFF_MS); + return result; + } + + private static class UniqueKey implements Key { + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + // Do nothing. + } + } + + // Visible for testing. + static class Clock { + public long now() { + return SystemClock.currentThreadTimeMillis(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFiller.java b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFiller.java new file mode 100755 index 0000000..4556ed0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/BitmapPreFiller.java @@ -0,0 +1,83 @@ +package com.example.bumptech.glide.load.engine.prefill; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.engine.cache.MemoryCache; +import com.example.bumptech.glide.util.Util; + +import java.util.HashMap; +import java.util.Map; + +/** + * A class for pre-filling {@link Bitmap Bitmaps} in a + * {@link BitmapPool}. + */ +public final class BitmapPreFiller { + + private final MemoryCache memoryCache; + private final BitmapPool bitmapPool; + private final DecodeFormat defaultFormat; + private final Handler handler = new Handler(Looper.getMainLooper()); + + private BitmapPreFillRunner current; + + public BitmapPreFiller(MemoryCache memoryCache, BitmapPool bitmapPool, DecodeFormat defaultFormat) { + this.memoryCache = memoryCache; + this.bitmapPool = bitmapPool; + this.defaultFormat = defaultFormat; + } + + @SuppressWarnings("deprecation") + public void preFill(PreFillType.Builder... bitmapAttributeBuilders) { + if (current != null) { + current.cancel(); + } + + PreFillType[] bitmapAttributes = new PreFillType[bitmapAttributeBuilders.length]; + for (int i = 0; i < bitmapAttributeBuilders.length; i++) { + PreFillType.Builder builder = bitmapAttributeBuilders[i]; + if (builder.getConfig() == null) { + builder.setConfig( + defaultFormat == DecodeFormat.ALWAYS_ARGB_8888 || defaultFormat == DecodeFormat.PREFER_ARGB_8888 + ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); + } + bitmapAttributes[i] = builder.build(); + } + + PreFillQueue allocationOrder = generateAllocationOrder(bitmapAttributes); + current = new BitmapPreFillRunner(bitmapPool, memoryCache, allocationOrder); + handler.post(current); + } + + // Visible for testing. + PreFillQueue generateAllocationOrder(PreFillType[] preFillSizes) { + final int maxSize = memoryCache.getMaxSize() - memoryCache.getCurrentSize() + bitmapPool.getMaxSize(); + + int totalWeight = 0; + for (PreFillType size : preFillSizes) { + totalWeight += size.getWeight(); + } + + final float bytesPerWeight = maxSize / (float) totalWeight; + + Map attributeToCount = new HashMap(); + for (PreFillType size : preFillSizes) { + int bytesForSize = Math.round(bytesPerWeight * size.getWeight()); + int bytesPerBitmap = getSizeInBytes(size); + int bitmapsForSize = bytesForSize / bytesPerBitmap; + attributeToCount.put(size, bitmapsForSize); + } + + return new PreFillQueue(attributeToCount); + } + + private static int getSizeInBytes(PreFillType size) { + return Util.getBitmapByteSize(size.getWidth(), size.getHeight(), size.getConfig()); + } +} + diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillQueue.java b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillQueue.java new file mode 100755 index 0000000..844750e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillQueue.java @@ -0,0 +1,49 @@ +package com.example.bumptech.glide.load.engine.prefill; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +final class PreFillQueue { + + private final Map bitmapsPerType; + private final List keyList; + private int bitmapsRemaining; + private int keyIndex; + + public PreFillQueue(Map bitmapsPerType) { + this.bitmapsPerType = bitmapsPerType; + // We don't particularly care about the initial order. + keyList = new ArrayList(bitmapsPerType.keySet()); + + for (Integer count : bitmapsPerType.values()) { + bitmapsRemaining += count; + } + } + + public PreFillType remove() { + PreFillType result = keyList.get(keyIndex); + + Integer countForResult = bitmapsPerType.get(result); + if (countForResult == 1) { + bitmapsPerType.remove(result); + keyList.remove(keyIndex); + } else { + bitmapsPerType.put(result, countForResult - 1); + } + bitmapsRemaining--; + + // Avoid divide by 0. + keyIndex = keyList.isEmpty() ? 0 : (keyIndex + 1) % keyList.size(); + + return result; + } + + public int getSize() { + return bitmapsRemaining; + } + + public boolean isEmpty() { + return bitmapsRemaining == 0; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillType.java b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillType.java new file mode 100755 index 0000000..8296099 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/engine/prefill/PreFillType.java @@ -0,0 +1,172 @@ +package com.example.bumptech.glide.load.engine.prefill; + +import android.graphics.Bitmap; + +/** + * A container for a set of options used to pre-fill a {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} + * with {@link Bitmap Bitmaps} of a single size and configuration. + */ +public final class PreFillType { + // Visible for testing. + static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.RGB_565; + private final int width; + private final int height; + private final Bitmap.Config config; + private final int weight; + + /** + * Constructor for a single type of {@link Bitmap}. + * + * @param width The width in pixels of the {@link Bitmap Bitmaps} to + * pre-fill. + * @param height The height in pixels of the {@link Bitmap Bitmaps} to + * pre-fill. + * @param config The {@link Bitmap.Config} of the {@link Bitmap Bitmaps} to + * pre-fill. + * @param weight An integer indicating how to balance pre-filling this size and configuration of + * {@link Bitmap} against any other sizes/configurations that may be being pre-filled. + */ + PreFillType(int width, int height, Bitmap.Config config, int weight) { + if (config == null) { + throw new NullPointerException("Config must not be null"); + } + + this.width = width; + this.height = height; + this.config = config; + this.weight = weight; + } + + /** + * Returns the width in pixels of the {@link Bitmap Bitmaps}. + */ + int getWidth() { + return width; + } + + /** + * Returns the height in pixels of the {@link Bitmap Bitmaps}. + */ + int getHeight() { + return height; + } + + /** + * Returns the {@link Bitmap.Config} of the {@link Bitmap Bitmaps}. + */ + Bitmap.Config getConfig() { + return config; + } + + /** + * Returns the weight of the {@link Bitmap Bitmaps} of this type. + */ + int getWeight() { + return weight; + } + + @Override + public boolean equals(Object o) { + if (o instanceof PreFillType) { + PreFillType other = (PreFillType) o; + return height == other.height + && width == other.width + && weight == other.weight + && config == other.config; + } + return false; + } + + @Override + public int hashCode() { + int result = width; + result = 31 * result + height; + result = 31 * result + config.hashCode(); + result = 31 * result + weight; + return result; + } + + @Override + public String toString() { + return "PreFillSize{" + + "width=" + width + + ", height=" + height + + ", config=" + config + + ", weight=" + weight + + '}'; + } + + /** + * Builder for {@link PreFillType}. + */ + public static class Builder { + private final int width; + private final int height; + + private Bitmap.Config config; + private int weight = 1; + + /** + * Constructor for a builder that uses the given size as the width and height of the Bitmaps to prefill. + * @param size The width and height in pixels of the Bitmaps to prefill. + */ + public Builder(int size) { + this(size, size); + } + + /** + * Constructor for a builder that uses the given dimensions as the dimensions of the Bitmaps to prefill. + * @param width The width in pixels of the Bitmaps to prefill. + * @param height The height in pixels of the Bitmaps to prefill. + */ + public Builder(int width, int height) { + if (width <= 0) { + throw new IllegalArgumentException("Width must be > 0"); + } + if (height <= 0) { + throw new IllegalArgumentException("Height must be > 0"); + } + this.width = width; + this.height = height; + } + + /** + * Sets the {@link Bitmap.Config} for the Bitmaps to pre-fill. + * @param config The config to use, or null to use Glide's default. + * @return This builder. + */ + public Builder setConfig(Bitmap.Config config) { + this.config = config; + return this; + } + + /** + * Returns the current {@link Bitmap.Config}. + */ + Bitmap.Config getConfig() { + return config; + } + + /** + * Sets the weight to use to balance how many Bitmaps of this type are prefilled relative to the other requested + * types. + * @param weight An integer indicating how to balance pre-filling this size and configuration of + * {@link Bitmap} against any other sizes/configurations that may be being pre-filled. + * @return This builder. + */ + public Builder setWeight(int weight) { + if (weight <= 0) { + throw new IllegalArgumentException("Weight must be > 0"); + } + this.weight = weight; + return this; + } + + /** + * Returns a new {@link PreFillType}. + */ + PreFillType build() { + return new PreFillType(width, height, config, weight); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/AssetUriParser.java b/core/src/main/java/com/example/bumptech/glide/load/model/AssetUriParser.java new file mode 100755 index 0000000..4914f0b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/AssetUriParser.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.model; + +import android.content.ContentResolver; +import android.net.Uri; + +/** + * A utility class for parsing Asset uris that look like: file:///android_asset/some/path/in/assets/folder. + */ +final class AssetUriParser { + private static final String ASSET_PATH_SEGMENT = "android_asset"; + private static final String ASSET_PREFIX = ContentResolver.SCHEME_FILE + ":///" + ASSET_PATH_SEGMENT + "/"; + private static final int ASSET_PREFIX_LENGTH = ASSET_PREFIX.length(); + + private AssetUriParser() { + // Utility constructor. + } + + /** + * Returns true if the given {@link Uri} matches the asset uri pattern. + */ + public static boolean isAssetUri(Uri uri) { + return ContentResolver.SCHEME_FILE.equals(uri.getScheme()) && !uri.getPathSegments().isEmpty() + && ASSET_PATH_SEGMENT.equals(uri.getPathSegments().get(0)); + } + + /** + * Returns the string path for the given asset uri. + * + *

+ * Assumes the given {@link Uri} is in fact an asset uri. + *

+ */ + public static String toAssetPath(Uri uri) { + return uri.toString().substring(ASSET_PREFIX_LENGTH); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/FileLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/FileLoader.java new file mode 100755 index 0000000..adfee1c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/FileLoader.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.load.model; + +import android.net.Uri; + + +import com.example.bumptech.glide.load.data.DataFetcher; + +import java.io.File; + +/** + * A simple model loader for loading data from {@link File}s. + * + * @param The type of data loaded from the given {@link File} ({@link java.io.InputStream} or + * {@link java.io.FileDescriptor} etc). + */ +public class FileLoader implements ModelLoader { + + private final ModelLoader uriLoader; + + public FileLoader(ModelLoader uriLoader) { + this.uriLoader = uriLoader; + } + + @Override + public DataFetcher getResourceFetcher(File model, int width, int height) { + return uriLoader.getResourceFetcher(Uri.fromFile(model), width, height); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/GenericLoaderFactory.java b/core/src/main/java/com/example/bumptech/glide/load/model/GenericLoaderFactory.java new file mode 100755 index 0000000..a55fc3e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/GenericLoaderFactory.java @@ -0,0 +1,202 @@ +package com.example.bumptech.glide.load.model; + +import android.content.Context; + + +import com.example.bumptech.glide.load.data.DataFetcher; + +import java.util.HashMap; +import java.util.Map; + +/** + * Maintains a map of model class to factory to retrieve a {@link ModelLoaderFactory} and/or a {@link ModelLoader} + * for a given model type. + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +// this is a general class capable of handling any generic combination +public class GenericLoaderFactory { + private final Map> modelClassToResourceFactories = + new HashMap>(); + private final Map> cachedModelLoaders = + new HashMap>(); + + private static final ModelLoader NULL_MODEL_LOADER = new ModelLoader() { + @Override + public DataFetcher getResourceFetcher(Object model, int width, int height) { + throw new NoSuchMethodError("This should never be called!"); + } + + @Override + public String toString() { + return "NULL_MODEL_LOADER"; + } + }; + + private final Context context; + + public GenericLoaderFactory(Context context) { + this.context = context.getApplicationContext(); + } + + /** + * Removes and returns the registered {@link ModelLoaderFactory} for the given model and resource classes. Returns + * null if no such factory is registered. Clears all cached model loaders. + * + * @param modelClass The model class. + * @param resourceClass The resource class. + * @param The type of the model the class. + * @param The type of the resource class. + */ + public synchronized ModelLoaderFactory unregister(Class modelClass, Class resourceClass) { + cachedModelLoaders.clear(); + + ModelLoaderFactory/*T, Y*/ result = null; + Map resourceToFactories = modelClassToResourceFactories.get(modelClass); + if (resourceToFactories != null) { + result = resourceToFactories.remove(resourceClass); + } + return result; + } + + /** + * Registers the given {@link ModelLoaderFactory} for the given model and resource classes and returns the previous + * factory registered for the given model and resource classes or null if no such factory existed. Clears all cached + * model loaders. + * + * @param modelClass The model class. + * @param resourceClass The resource class. + * @param factory The factory to register. + * @param The type of the model. + * @param The type of the resource. + */ + public synchronized ModelLoaderFactory register(Class modelClass, Class resourceClass, + ModelLoaderFactory factory) { + cachedModelLoaders.clear(); + + Map resourceToFactories = modelClassToResourceFactories.get(modelClass); + if (resourceToFactories == null) { + resourceToFactories = new HashMap(); + modelClassToResourceFactories.put(modelClass, resourceToFactories); + } + + ModelLoaderFactory/*T, Y*/ previous = resourceToFactories.put(resourceClass, factory); + + if (previous != null) { + // This factory may be being used by another model. We don't want to say it has been removed unless we + // know it has been removed for all models. + for (Map factories : modelClassToResourceFactories.values()) { + if (factories.containsValue(previous)) { + previous = null; + break; + } + } + } + + return previous; + } + + /** + * Returns a {@link ModelLoader} for the given model and resource classes by either returning a cached + * {@link ModelLoader} or building a new a new {@link ModelLoader} using registered {@link ModelLoaderFactory}s. + * Returns null if no {@link ModelLoaderFactory} is registered for the given classes. + * + * @deprecated Use {@link #buildModelLoader(Class, Class)} instead. Scheduled to be removed in Glide 4.0. + * @param modelClass The model class. + * @param resourceClass The resource class. + * @param context Unused + * @param The type of the model. + * @param The type of the resource. + */ + @Deprecated + public synchronized ModelLoader buildModelLoader(Class modelClass, Class resourceClass, + Context context) { + return buildModelLoader(modelClass, resourceClass); + } + + /** + * Returns a {@link ModelLoader} for the given model and resource classes by either returning a cached + * {@link ModelLoader} or building a new a new {@link ModelLoader} using registered {@link ModelLoaderFactory}s. + * Returns null if no {@link ModelLoaderFactory} is registered for the given classes. + * + * @param modelClass The model class. + * @param resourceClass The resource class. + * @param The type of the model. + * @param The type of the resource. + */ + public synchronized ModelLoader buildModelLoader(Class modelClass, Class resourceClass) { + ModelLoader result = getCachedLoader(modelClass, resourceClass); + if (result != null) { + // We've already tried to create a model loader and can't with the currently registered set of factories, + // but we can't use null to demonstrate that failure because model loaders that haven't been requested + // yet will be null in the cache. To avoid this, we use a special signal model loader. + if (NULL_MODEL_LOADER.equals(result)) { + return null; + } else { + return result; + } + } + + final ModelLoaderFactory factory = getFactory(modelClass, resourceClass); + if (factory != null) { + result = factory.build(context, this); + cacheModelLoader(modelClass, resourceClass, result); + } else { + // We can't generate a model loader for the given arguments with the currently registered set of factories. + cacheNullLoader(modelClass, resourceClass); + } + return result; + } + + private void cacheNullLoader(Class modelClass, Class resourceClass) { + cacheModelLoader(modelClass, resourceClass, NULL_MODEL_LOADER); + } + + private void cacheModelLoader(Class modelClass, Class resourceClass, ModelLoader modelLoader) { + Map resourceToLoaders = cachedModelLoaders.get(modelClass); + if (resourceToLoaders == null) { + resourceToLoaders = new HashMap(); + cachedModelLoaders.put(modelClass, resourceToLoaders); + } + resourceToLoaders.put(resourceClass, modelLoader); + } + + private ModelLoader getCachedLoader(Class modelClass, Class resourceClass) { + Map resourceToLoaders = cachedModelLoaders.get(modelClass); + ModelLoader/*T, Y*/ result = null; + if (resourceToLoaders != null) { + result = resourceToLoaders.get(resourceClass); + } + + return result; + } + + private ModelLoaderFactory getFactory(Class modelClass, Class resourceClass) { + Map resourceToFactories = modelClassToResourceFactories.get(modelClass); + ModelLoaderFactory/*T, Y*/ result = null; + if (resourceToFactories != null) { + result = resourceToFactories.get(resourceClass); + } + + if (result == null) { + for (Class registeredModelClass : modelClassToResourceFactories.keySet()) { + // This accounts for model subclasses, our map only works for exact matches. We should however still + // match a subclass of a model with a factory for a super class of that model if if there isn't a + // factory for that particular subclass. Uris are a great example of when this happens, most uris + // are actually subclasses for Uri, but we'd generally rather load them all with the same factory rather + // than trying to register for each subclass individually. + if (registeredModelClass.isAssignableFrom(modelClass)) { + Map currentResourceToFactories = + modelClassToResourceFactories.get(registeredModelClass); + if (currentResourceToFactories != null) { + result = currentResourceToFactories.get(resourceClass); + if (result != null) { + break; + } + } + } + } + } + + return result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/GlideUrl.java b/core/src/main/java/com/example/bumptech/glide/load/model/GlideUrl.java new file mode 100755 index 0000000..0dca598 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/GlideUrl.java @@ -0,0 +1,146 @@ +package com.example.bumptech.glide.load.model; + +import android.net.Uri; +import android.text.TextUtils; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +/** + * A wrapper for strings representing http/https URLs responsible for ensuring URLs are properly escaped and avoiding + * unnecessary URL instantiations for loaders that require only string urls rather than URL objects. + * + *

Users wishing to replace the class for handling URLs must register a factory using GlideUrl.

+ * + *

To obtain a properly escaped URL, call {@link #toURL()}. To obtain a properly escaped string URL, call + * {@link #toStringUrl()}. To obtain a less safe, but less expensive to calculate cache key, call + * {@link #getCacheKey()}.

+ * + *

This class can also optionally wrap {@link Headers} for convenience.

+ */ +public class GlideUrl { + private static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%"; + + private final URL url; + private final Headers headers; + private final String stringUrl; + + private String safeStringUrl; + private URL safeUrl; + + public GlideUrl(URL url) { + this(url, Headers.DEFAULT); + } + + public GlideUrl(String url) { + this(url, Headers.DEFAULT); + } + + public GlideUrl(URL url, Headers headers) { + if (url == null) { + throw new IllegalArgumentException("URL must not be null!"); + } + if (headers == null) { + throw new IllegalArgumentException("Headers must not be null"); + } + this.url = url; + stringUrl = null; + this.headers = headers; + } + + public GlideUrl(String url, Headers headers) { + if (headers == null) { + throw new IllegalArgumentException("Headers must not be null"); + } + if (url != null) { + this.stringUrl = url; + } else { + this.stringUrl = "http://giffunnotexists.com/not_exists_image_giffun.jpg"; + } + this.url = null; + this.headers = headers; + } + + /** + * Returns a properly escaped {@link URL} that can be used to make http/https requests. + * + * @see #toStringUrl() + * @see #getCacheKey() + * @throws MalformedURLException + */ + public URL toURL() throws MalformedURLException { + return getSafeUrl(); + } + + // See http://stackoverflow.com/questions/3286067/url-encoding-in-android. Although the answer using URI would work, + // using it would require both decoding and encoding each string which is more complicated, slower and generates + // more objects than the solution below. See also issue #133. + private URL getSafeUrl() throws MalformedURLException { + if (safeUrl == null) { + safeUrl = new URL(getSafeStringUrl()); + } + return safeUrl; + } + + /** + * Returns a properly escaped {@link String} url that can be used to make http/https requests. + * + * @see #toURL() + * @see #getCacheKey() + */ + public String toStringUrl() { + return getSafeStringUrl(); + } + + private String getSafeStringUrl() { + if (TextUtils.isEmpty(safeStringUrl)) { + String unsafeStringUrl = stringUrl; + if (TextUtils.isEmpty(unsafeStringUrl)) { + unsafeStringUrl = url.toString(); + } + safeStringUrl = Uri.encode(unsafeStringUrl, ALLOWED_URI_CHARS); + } + return safeStringUrl; + } + + /** + * Returns a non-null {@link Map} containing headers. + */ + public Map getHeaders() { + return headers.getHeaders(); + } + + /** + * Returns an inexpensive to calculate {@link String} suitable for use as a disk cache key. + * + *

This method does not include headers.

+ * + *

Unlike {@link #toStringUrl()}} and {@link #toURL()}, this method does not escape input.

+ */ + public String getCacheKey() { + return stringUrl != null ? stringUrl : url.toString(); + } + + @Override + public String toString() { + return getCacheKey() + '\n' + headers.toString(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof GlideUrl) { + GlideUrl other = (GlideUrl) o; + return getCacheKey().equals(other.getCacheKey()) + && headers.equals(other.headers); + } + return false; + } + + @Override + public int hashCode() { + int hashCode = getCacheKey().hashCode(); + hashCode = 31 * hashCode + headers.hashCode(); + return hashCode; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/Headers.java b/core/src/main/java/com/example/bumptech/glide/load/model/Headers.java new file mode 100755 index 0000000..a43d2e0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/Headers.java @@ -0,0 +1,33 @@ +package com.example.bumptech.glide.load.model; + +import java.util.Collections; +import java.util.Map; + +/** + * An interface for a wrapper for a set of headers to be included in a Glide request. + * Implementations must implement equals() and hashcode(). + */ +public interface Headers { + + /** + * An empty Headers object that can be used if users don't want to provide headers. + * + * @deprecated Use {@link #DEFAULT} instead. + */ + @Deprecated + Headers NONE = new Headers() { + @Override + public Map getHeaders() { + return Collections.emptyMap(); + } + }; + + /** + * A Headers object containing reasonable defaults that should be used when users don't want + * to provide their own headers. + */ + Headers DEFAULT = new LazyHeaders.Builder().build(); + + Map getHeaders(); + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoModelLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoModelLoader.java new file mode 100755 index 0000000..4a39b94 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoModelLoader.java @@ -0,0 +1,127 @@ +package com.example.bumptech.glide.load.model; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.data.DataFetcher; + +import java.io.InputStream; + +/** + * A wrapper model loader that provides both an {@link InputStream} and a + * {@link ParcelFileDescriptor} for a given model type by wrapping an + * {@link ModelLoader} for {@link InputStream}s for the given model type and an + * {@link ModelLoader} for {@link ParcelFileDescriptor} for the given model + * type. + * + * @param The model type. + */ +public class ImageVideoModelLoader implements ModelLoader { + private static final String TAG = "IVML"; + + private final ModelLoader streamLoader; + private final ModelLoader fileDescriptorLoader; + + public ImageVideoModelLoader(ModelLoader streamLoader, + ModelLoader fileDescriptorLoader) { + if (streamLoader == null && fileDescriptorLoader == null) { + throw new NullPointerException("At least one of streamLoader and fileDescriptorLoader must be non null"); + } + this.streamLoader = streamLoader; + this.fileDescriptorLoader = fileDescriptorLoader; + } + + @Override + public DataFetcher getResourceFetcher(A model, int width, int height) { + DataFetcher streamFetcher = null; + if (streamLoader != null) { + streamFetcher = streamLoader.getResourceFetcher(model, width, height); + } + DataFetcher fileDescriptorFetcher = null; + if (fileDescriptorLoader != null) { + fileDescriptorFetcher = fileDescriptorLoader.getResourceFetcher(model, width, height); + } + + if (streamFetcher != null || fileDescriptorFetcher != null) { + return new ImageVideoFetcher(streamFetcher, fileDescriptorFetcher); + } else { + return null; + } + } + + static class ImageVideoFetcher implements DataFetcher { + private final DataFetcher streamFetcher; + private final DataFetcher fileDescriptorFetcher; + + public ImageVideoFetcher(DataFetcher streamFetcher, + DataFetcher fileDescriptorFetcher) { + this.streamFetcher = streamFetcher; + this.fileDescriptorFetcher = fileDescriptorFetcher; + } + + @SuppressWarnings("resource") + // @see ModelLoader.loadData + @Override + public ImageVideoWrapper loadData(Priority priority) throws Exception { + InputStream is = null; + if (streamFetcher != null) { + try { + is = streamFetcher.loadData(priority); + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Exception fetching input stream, trying ParcelFileDescriptor", e); + } + if (fileDescriptorFetcher == null) { + throw e; + } + } + } + ParcelFileDescriptor fileDescriptor = null; + if (fileDescriptorFetcher != null) { + try { + fileDescriptor = fileDescriptorFetcher.loadData(priority); + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Exception fetching ParcelFileDescriptor", e); + } + if (is == null) { + throw e; + } + } + } + return new ImageVideoWrapper(is, fileDescriptor); + } + + @Override + public void cleanup() { + //TODO: what if this throws? + if (streamFetcher != null) { + streamFetcher.cleanup(); + } + if (fileDescriptorFetcher != null) { + fileDescriptorFetcher.cleanup(); + } + } + + @Override + public String getId() { + // Both the stream fetcher and the file descriptor fetcher should return the same id. + if (streamFetcher != null) { + return streamFetcher.getId(); + } else { + return fileDescriptorFetcher.getId(); + } + } + + @Override + public void cancel() { + if (streamFetcher != null) { + streamFetcher.cancel(); + } + if (fileDescriptorFetcher != null) { + fileDescriptorFetcher.cancel(); + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapper.java b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapper.java new file mode 100755 index 0000000..16478ce --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapper.java @@ -0,0 +1,26 @@ +package com.example.bumptech.glide.load.model; + +import android.os.ParcelFileDescriptor; + +import java.io.InputStream; + +/** + * A simple wrapper that wraps an {@link InputStream} and/or an {@link ParcelFileDescriptor}. + */ +public class ImageVideoWrapper { + private final InputStream streamData; + private final ParcelFileDescriptor fileDescriptor; + + public ImageVideoWrapper(InputStream streamData, ParcelFileDescriptor fileDescriptor) { + this.streamData = streamData; + this.fileDescriptor = fileDescriptor; + } + + public InputStream getStream() { + return streamData; + } + + public ParcelFileDescriptor getFileDescriptor() { + return fileDescriptor; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapperEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapperEncoder.java new file mode 100755 index 0000000..9ba47ed --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ImageVideoWrapperEncoder.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.model; + +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.Encoder; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A source encoder that writes a {@link ImageVideoWrapper} to disk by preferentially + * writing data from the wrapper's {@link InputStream} and falling back to the wrapper's + * {@link ParcelFileDescriptor} if the {@link InputStream} isn't available. + */ +public class ImageVideoWrapperEncoder implements Encoder { + private final Encoder streamEncoder; + private final Encoder fileDescriptorEncoder; + private String id; + + public ImageVideoWrapperEncoder(Encoder streamEncoder, + Encoder fileDescriptorEncoder) { + this.streamEncoder = streamEncoder; + this.fileDescriptorEncoder = fileDescriptorEncoder; + } + + @Override + public boolean encode(ImageVideoWrapper data, OutputStream os) { + if (data.getStream() != null) { + return streamEncoder.encode(data.getStream(), os); + } else { + return fileDescriptorEncoder.encode(data.getFileDescriptor(), os); + } + } + + @Override + public String getId() { + if (id == null) { + id = streamEncoder.getId() + fileDescriptorEncoder.getId(); + } + return id; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaderFactory.java b/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaderFactory.java new file mode 100755 index 0000000..80dad06 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaderFactory.java @@ -0,0 +1,13 @@ +package com.example.bumptech.glide.load.model; + +/** + * An interface for lazily creating headers that allows expensive to calculate headers (oauth for + * example) to be generated in the background during the first fetch. + * + *

Implementations should implement equals() and hashcode()

. + */ +public interface LazyHeaderFactory { + + String buildHeader(); + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaders.java b/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaders.java new file mode 100755 index 0000000..7f72174 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/LazyHeaders.java @@ -0,0 +1,263 @@ +package com.example.bumptech.glide.load.model; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A wrapper class for a set of headers to be included in a Glide request, allowing headers to be + * constructed lazily. + * + *

Ideally headers are constructed once and then re-used for multiple loads, rather then being + * constructed individually for each load.

+ * + *

This class is thread safe.

+ */ +public final class LazyHeaders implements Headers { + private final Map> headers; + private volatile Map combinedHeaders; + + LazyHeaders(Map> headers) { + this.headers = Collections.unmodifiableMap(headers); + } + + @Override + public Map getHeaders() { + if (combinedHeaders == null) { + synchronized (this) { + if (combinedHeaders == null) { + this.combinedHeaders = Collections.unmodifiableMap(generateHeaders()); + } + } + } + + return combinedHeaders; + } + + private Map generateHeaders() { + Map combinedHeaders = new HashMap(); + + for (Map.Entry> entry : headers.entrySet()) { + StringBuilder sb = new StringBuilder(); + List factories = entry.getValue(); + for (int i = 0; i < factories.size(); i++) { + LazyHeaderFactory factory = factories.get(i); + sb.append(factory.buildHeader()); + if (i != factories.size() - 1) { + sb.append(','); + } + } + combinedHeaders.put(entry.getKey(), sb.toString()); + } + + return combinedHeaders; + } + + @Override + public String toString() { + return "LazyHeaders{" + + "headers=" + headers + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o instanceof LazyHeaders) { + LazyHeaders other = (LazyHeaders) o; + return headers.equals(other.headers); + } + return false; + } + + @Override + public int hashCode() { + return headers.hashCode(); + } + + /** + * Builder class for {@link LazyHeaders}. + * + *

This class is not thread safe.

+ * + *

This class may include default values for User-Agent and Accept-Encoding headers. These + * will be replaced by calls to either {@link #setHeader(String, LazyHeaderFactory)} or + * {@link #addHeader(String, String)}, even though {@link #addHeader(String, LazyHeaderFactory)} + * would usually append an additional value.

+ */ + // PMD doesn't like the necessary static block to initialize DEFAULT_HEADERS. + @SuppressWarnings("PMD.FieldDeclarationsShouldBeAtStartOfClass") + public static final class Builder { + private static final String USER_AGENT_HEADER = "User-Agent"; + private static final String DEFAULT_USER_AGENT = System.getProperty("http.agent"); + private static final String ENCODING_HEADER = "Accept-Encoding"; + private static final String DEFAULT_ENCODING = "identity"; + private static final Map> DEFAULT_HEADERS; + + // Set Accept-Encoding header to do our best to avoid gzip since it's both inefficient for + // images and also makes it more difficult for us to detect and prevent partial content + // rendering. See #440. + static { + Map> temp + = new HashMap>(2); + if (!TextUtils.isEmpty(DEFAULT_USER_AGENT)) { + temp.put(USER_AGENT_HEADER, + Collections.singletonList( + new StringHeaderFactory(DEFAULT_USER_AGENT))); + } + temp.put(ENCODING_HEADER, + Collections.singletonList( + new StringHeaderFactory(DEFAULT_ENCODING))); + DEFAULT_HEADERS = Collections.unmodifiableMap(temp); + } + + private boolean copyOnModify = true; + private Map> headers = DEFAULT_HEADERS; + private boolean isEncodingDefault = true; + private boolean isUserAgentDefault = true; + + /** + * Adds a value for the given header and returns this builder. + * + *

Use {@link #addHeader(String, LazyHeaderFactory)} if obtaining the value requires I/O + * (ie an oauth token).

+ * + * @see #addHeader(String, LazyHeaderFactory) + + */ + public Builder addHeader(String key, String value) { + return addHeader(key, new StringHeaderFactory(value)); + } + + /** + * Adds an {@link LazyHeaderFactory} that will be used to construct a value for the given + * key lazily on a background thread. + * + *

Headers may have multiple values whose order is defined by the order in which + * this method is called.

+ * + *

This class does not prevent you from adding the same value to a given key multiple + * times

+ */ + public Builder addHeader(String key, LazyHeaderFactory factory) { + if ((isEncodingDefault && ENCODING_HEADER.equalsIgnoreCase(key)) + || (isUserAgentDefault && USER_AGENT_HEADER.equalsIgnoreCase(key))) { + return setHeader(key, factory); + } + + copyIfNecessary(); + getFactories(key).add(factory); + return this; + } + + /** + * Replaces all existing {@link LazyHeaderFactory LazyHeaderFactorys} for the given key + * with the given {@link LazyHeaderFactory}. + * + *

If the given value is {@code null}, the header at the given key will be removed.

+ * + *

Use {@link #setHeader(String, LazyHeaderFactory)} if obtaining the value requires I/O + * (ie an oauth token).

+ */ + public Builder setHeader(String key, String value) { + return setHeader(key, value == null ? null : new StringHeaderFactory(value)); + } + + /** + * Replaces all existing {@link LazyHeaderFactory LazyHeaderFactorys} for the given key + * with the given {@link LazyHeaderFactory}. + * + *

If the given value is {@code null}, the header at the given key will be removed.

+ */ + public Builder setHeader(String key, LazyHeaderFactory factory) { + copyIfNecessary(); + if (factory == null) { + headers.remove(key); + } else { + List factories = getFactories(key); + factories.clear(); + factories.add(factory); + } + + if (isEncodingDefault && ENCODING_HEADER.equalsIgnoreCase(key)) { + isEncodingDefault = false; + } + if (isUserAgentDefault && USER_AGENT_HEADER.equalsIgnoreCase(key)) { + isUserAgentDefault = false; + } + + return this; + } + + private List getFactories(String key) { + List factories = headers.get(key); + if (factories == null) { + factories = new ArrayList(); + headers.put(key, factories); + } + return factories; + } + + private void copyIfNecessary() { + if (copyOnModify) { + copyOnModify = false; + headers = copyHeaders(); + } + } + + /** + * Returns a new immutable {@link LazyHeaders} object. + */ + public LazyHeaders build() { + copyOnModify = true; + return new LazyHeaders(headers); + } + + private Map> copyHeaders() { + Map> result = + new HashMap>(headers.size()); + for (Map.Entry> entry : headers.entrySet()) { + result.put(entry.getKey(), new ArrayList(entry.getValue())); + } + return result; + } + } + + static final class StringHeaderFactory implements LazyHeaderFactory { + + private final String value; + + StringHeaderFactory(String value) { + this.value = value; + } + + @Override + public String buildHeader() { + return value; + } + + @Override + public String toString() { + return "StringHeaderFactory{" + + "value='" + value + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o instanceof StringHeaderFactory) { + StringHeaderFactory other = (StringHeaderFactory) o; + return value.equals(other.value); + } + return false; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ModelCache.java b/core/src/main/java/com/example/bumptech/glide/load/model/ModelCache.java new file mode 100755 index 0000000..6102d15 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ModelCache.java @@ -0,0 +1,112 @@ +package com.example.bumptech.glide.load.model; + + +import com.example.bumptech.glide.util.LruCache; +import com.example.bumptech.glide.util.Util; + +import java.util.Queue; + +/** + * A simple cache that can be used by {@link ModelLoader} and {@link ModelLoaderFactory} to cache some data for a given + * model, width and height. For a loader that takes a model and returns a url, the cache could be used to safely memoize + * url creation based on the width and height of the view. + * + * @param
Some Model type that implements {@link #equals} and {@link #hashCode}. + * @param Some useful type that may be expensive to create (URL, file path, etc). + */ +public class ModelCache { + private static final int DEFAULT_SIZE = 250; + + private final LruCache, B> cache; + + public ModelCache() { + this(DEFAULT_SIZE); + } + + public ModelCache(int size) { + cache = new LruCache, B>(size) { + @Override + protected void onItemEvicted(ModelKey key, B item) { + key.release(); + } + }; + } + + /** + * Get a value. + * + * @param model The model. + * @param width The width in pixels of the view the image is being loaded into. + * @param height The height in pixels of the view the image is being loaded into. + * + * @return The cached result, or null. + */ + public B get(A model, int width, int height) { + ModelKey key = ModelKey.get(model, width, height); + B result = cache.get(key); + key.release(); + return result; + } + + /** + * Add a value. + * + * @param model The model. + * @param width The width in pixels of the view the image is being loaded into. + * @param height The height in pixels of the view the image is being loaded into. + * @param value The value to store. + */ + public void put(A model, int width, int height, B value) { + ModelKey key = ModelKey.get(model, width, height); + cache.put(key, value); + } + + // Visible for testing. + static final class ModelKey { + private static final Queue> KEY_QUEUE = Util.createQueue(0); + + private int height; + private int width; + private A model; + + static ModelKey get(A model, int width, int height) { + @SuppressWarnings("unchecked") + ModelKey modelKey = (ModelKey) KEY_QUEUE.poll(); + if (modelKey == null) { + modelKey = new ModelKey(); + } + + modelKey.init(model, width, height); + return modelKey; + } + + private ModelKey() { } + + private void init(A model, int width, int height) { + this.model = model; + this.width = width; + this.height = height; + } + + public void release() { + KEY_QUEUE.offer(this); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ModelKey) { + ModelKey other = (ModelKey) o; + return width == other.width && height == other.height && model.equals(other.model); + } + return false; + } + + @Override + public int hashCode() { + int result = height; + result = 31 * result + width; + result = 31 * result + model.hashCode(); + return result; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoader.java new file mode 100755 index 0000000..53da56d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoader.java @@ -0,0 +1,50 @@ +package com.example.bumptech.glide.load.model; + +import com.example.bumptech.glide.load.data.DataFetcher; + +/** + * A factory interface for translating an arbitrarily complex data model into a concrete data type that can be used + * by an {@link DataFetcher} to obtain the data for a resource represented by the model. + * + *

+ * This interface has two objectives: + * 1. To translate a specific model into a data type that can be decoded into a resource. + * + * 2. To allow a model to be combined with the dimensions of the view to fetch a resource of a specific size. + * + * This not only avoids having to duplicate dimensions in xml and in your code in order to determine the size of a + * view on devices with different densities, but also allows you to use layout weights or otherwise + * programatically set the dimensions of the view without forcing you to fetch a generic resource size. + * + * The smaller the resource you fetch, the less bandwidth and battery life you use, and the lower your memory + * footprint per resource. + *

+ * + * @param The type of the model. + * @param The type of the data that can be used by a {@link com.bumptech.glide.load.ResourceDecoder} to decode a + * resource. + */ +public interface ModelLoader { + + /** + * Obtains an {@link DataFetcher} that can fetch the data required to decode the resource represented by this model. + * The {@link DataFetcher} will not be used if the resource is already cached. + * + *

+ * Note - If no valid data fetcher can be returned (for example if a model has a null URL), then it is + * acceptable to return a null data fetcher from this method. Doing so will be treated any other failure or + * exception during the load process. + *

+ * + * @param model The model representing the resource. + * @param width The width in pixels of the view or target the resource will be loaded into, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the resource should + * be loaded at its original width. + * @param height The height in pixels of the view or target the resource will be loaded into, or + * {@link com.bumptech.glide.request.target.Target#SIZE_ORIGINAL} to indicate that the resource should + * be loaded at its original height. + * @return A {@link DataFetcher} that can obtain the data the resource can be decoded from if the resource is not + * cached, or null if no valid {@link DataFetcher} could be constructed. + */ + DataFetcher getResourceFetcher(T model, int width, int height); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoaderFactory.java b/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoaderFactory.java new file mode 100755 index 0000000..1a14150 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ModelLoaderFactory.java @@ -0,0 +1,31 @@ +package com.example.bumptech.glide.load.model; + +import android.content.Context; + +/** + * An interface for creating a {@link ModelLoader} for a given model type. Will be retained statically so should not + * retain {@link Context} or any other objects that cannot be retained for the life of the application. ModelLoaders + * will not be retained statically so it is safe for any ModelLoader built by this factory to retain a reference to a + * {@link Context}. + * + * @param The type of the model the {@link ModelLoader}s built by this factory + * can handle + * @param The type of data the {@link ModelLoader}s built by this factory can load. + */ +public interface ModelLoaderFactory { + + /** + * Build a concrete ModelLoader for this model type. + * + * @param context A context that cannot be retained by the factory but can be retained by the {@link ModelLoader} + * @param factories A map of classes to factories that can be used to construct additional {@link ModelLoader}s that + * this factory's {@link ModelLoader} may depend on + * @return A new {@link ModelLoader} + */ + ModelLoader build(Context context, GenericLoaderFactory factories); + + /** + * A lifecycle method that will be called when this factory is about to replaced. + */ + void teardown(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/ResourceLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/ResourceLoader.java new file mode 100755 index 0000000..ec71c44 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/ResourceLoader.java @@ -0,0 +1,53 @@ +package com.example.bumptech.glide.load.model; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.util.Log; + +import com.example.bumptech.glide.load.data.DataFetcher; + + +/** + * A model loader for handling Android resource files. Model must be an Android resource id in the package of the given + * context. + * + * @param The type of data that will be loaded for the given android resource. + */ +public class ResourceLoader implements ModelLoader { + private static final String TAG = "ResourceLoader"; + + private final ModelLoader uriLoader; + private final Resources resources; + + public ResourceLoader(Context context, ModelLoader uriLoader) { + this(context.getResources(), uriLoader); + } + + public ResourceLoader(Resources resources, ModelLoader uriLoader) { + this.resources = resources; + this.uriLoader = uriLoader; + } + + @Override + public DataFetcher getResourceFetcher(Integer model, int width, int height) { + Uri uri = null; + try { + uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + resources.getResourcePackageName(model) + '/' + + resources.getResourceTypeName(model) + '/' + + resources.getResourceEntryName(model)); + } catch (Resources.NotFoundException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Received invalid resource id: " + model, e); + } + } + + if (uri != null) { + return uriLoader.getResourceFetcher(uri, width, height); + } else { + return null; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/StreamEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/model/StreamEncoder.java new file mode 100755 index 0000000..64c37f1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/StreamEncoder.java @@ -0,0 +1,42 @@ +package com.example.bumptech.glide.load.model; + +import android.util.Log; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.util.ByteArrayPool; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * An {@link Encoder} that can write an {@link InputStream} to disk. + */ +public class StreamEncoder implements Encoder { + private static final String TAG = "StreamEncoder"; + + @Override + public boolean encode(InputStream data, OutputStream os) { + byte[] buffer = ByteArrayPool.get().getBytes(); + try { + int read; + while ((read = data.read(buffer)) != -1) { + os.write(buffer, 0, read); + } + return true; + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to encode data onto the OutputStream", e); + } + return false; + } finally { + ByteArrayPool.get().releaseBytes(buffer); + } + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/StringLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/StringLoader.java new file mode 100755 index 0000000..edb8add --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/StringLoader.java @@ -0,0 +1,45 @@ +package com.example.bumptech.glide.load.model; + +import android.net.Uri; +import android.text.TextUtils; + + +import com.example.bumptech.glide.load.data.DataFetcher; + +import java.io.File; + +/** + * A model loader for handling certain string models. Handles paths, urls, and any uri string with a scheme handled by + * {@link android.content.ContentResolver#openInputStream(Uri)}. + * + * @param The type of data that will be loaded from the given {@link String}. + */ +public class StringLoader implements ModelLoader { + private final ModelLoader uriLoader; + + public StringLoader(ModelLoader uriLoader) { + this.uriLoader = uriLoader; + } + + @Override + public DataFetcher getResourceFetcher(String model, int width, int height) { + Uri uri; + if (TextUtils.isEmpty(model)) { + return null; + } else if (model.startsWith("/")) { + uri = toFileUri(model); + } else { + uri = Uri.parse(model); + final String scheme = uri.getScheme(); + if (scheme == null) { + uri = toFileUri(model); + } + } + + return uriLoader.getResourceFetcher(uri, width, height); + } + + private static Uri toFileUri(String path) { + return Uri.fromFile(new File(path)); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/UriLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/UriLoader.java new file mode 100755 index 0000000..8e9035e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/UriLoader.java @@ -0,0 +1,53 @@ +package com.example.bumptech.glide.load.model; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; + +import com.example.bumptech.glide.load.data.DataFetcher; + +/** + * A base ModelLoader for {@link Uri}s that handles local {@link Uri}s directly and routes + * remote {@link Uri}s to a wrapped {@link ModelLoader} that handles + * {@link GlideUrl}s. + * + * @param The type of data that will be retrieved for {@link Uri}s. + */ +public abstract class UriLoader implements ModelLoader { + private final Context context; + private final ModelLoader urlLoader; + + public UriLoader(Context context, ModelLoader urlLoader) { + this.context = context; + this.urlLoader = urlLoader; + } + + @Override + public final DataFetcher getResourceFetcher(Uri model, int width, int height) { + final String scheme = model.getScheme(); + + DataFetcher result = null; + if (isLocalUri(scheme)) { + if (AssetUriParser.isAssetUri(model)) { + String path = AssetUriParser.toAssetPath(model); + result = getAssetPathFetcher(context, path); + } else { + result = getLocalUriFetcher(context, model); + } + } else if (urlLoader != null && ("http".equals(scheme) || "https".equals(scheme))) { + result = urlLoader.getResourceFetcher(new GlideUrl(model.toString()), width, height); + } + + return result; + } + + protected abstract DataFetcher getLocalUriFetcher(Context context, Uri uri); + + protected abstract DataFetcher getAssetPathFetcher(Context context, String path); + + private static boolean isLocalUri(String scheme) { + return ContentResolver.SCHEME_FILE.equals(scheme) + || ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/UrlLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/UrlLoader.java new file mode 100755 index 0000000..293ceaa --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/UrlLoader.java @@ -0,0 +1,26 @@ +package com.example.bumptech.glide.load.model; + + +import com.example.bumptech.glide.load.data.DataFetcher; + +import java.net.URL; + +/** + * A wrapper class that translates {@link URL} objects into {@link GlideUrl} + * objects and then uses the wrapped {@link ModelLoader} for + * {@link GlideUrl}s to load the data. + * + * @param The type of data that will be loaded from the {@link URL}s. + */ +public class UrlLoader implements ModelLoader { + private final ModelLoader glideUrlLoader; + + public UrlLoader(ModelLoader glideUrlLoader) { + this.glideUrlLoader = glideUrlLoader; + } + + @Override + public DataFetcher getResourceFetcher(URL model, int width, int height) { + return glideUrlLoader.getResourceFetcher(new GlideUrl(model), width, height); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorFileLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorFileLoader.java new file mode 100755 index 0000000..e25d395 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorFileLoader.java @@ -0,0 +1,45 @@ +package com.example.bumptech.glide.load.model.file_descriptor; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.FileLoader; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; + +import java.io.File; + +/** + * A {@link ModelLoader} For translating {@link File} models into {@link ParcelFileDescriptor} data. + */ +public class FileDescriptorFileLoader extends FileLoader + implements FileDescriptorModelLoader { + + /** + * The default {@link ModelLoaderFactory} for + * {@link FileDescriptorFileLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new FileDescriptorFileLoader(factories.buildModelLoader(Uri.class, ParcelFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public FileDescriptorFileLoader(Context context) { + this(Glide.buildFileDescriptorModelLoader(Uri.class, context)); + } + + public FileDescriptorFileLoader(ModelLoader uriLoader) { + super(uriLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorModelLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorModelLoader.java new file mode 100755 index 0000000..a515f70 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorModelLoader.java @@ -0,0 +1,15 @@ +package com.example.bumptech.glide.load.model.file_descriptor; + +import android.os.ParcelFileDescriptor; + +import com.example.bumptech.glide.load.model.ModelLoader; + + +/** + * A base class for {@link ModelLoader}s that translate models into {@link java.io.File}s. + * + * @param The type of the model that will be translated into an {@link java.io.File}. + */ +public interface FileDescriptorModelLoader extends ModelLoader { + // specializing the generic arguments +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorResourceLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorResourceLoader.java new file mode 100755 index 0000000..47cfabf --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorResourceLoader.java @@ -0,0 +1,44 @@ +package com.example.bumptech.glide.load.model.file_descriptor; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.ResourceLoader; + + +/** + * A {@link ModelLoader} For translating android resource id models into {@link ParcelFileDescriptor} data. + */ +public class FileDescriptorResourceLoader extends ResourceLoader + implements FileDescriptorModelLoader { + + /** + * The default factory for {@link FileDescriptorResourceLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new FileDescriptorResourceLoader(context, factories.buildModelLoader(Uri.class, + ParcelFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public FileDescriptorResourceLoader(Context context) { + this(context, Glide.buildFileDescriptorModelLoader(Uri.class, context)); + } + + public FileDescriptorResourceLoader(Context context, ModelLoader uriLoader) { + super(context, uriLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorStringLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorStringLoader.java new file mode 100755 index 0000000..1eb154d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorStringLoader.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.model.file_descriptor; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.StringLoader; + + +/** + * A {@link ModelLoader} For translating {@link String} models, such as file paths, into {@link ParcelFileDescriptor} + * data. + */ +public class FileDescriptorStringLoader extends StringLoader + implements FileDescriptorModelLoader { + + /** + * The default factory for {@link FileDescriptorStringLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new FileDescriptorStringLoader(factories.buildModelLoader(Uri.class, ParcelFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public FileDescriptorStringLoader(Context context) { + this(Glide.buildFileDescriptorModelLoader(Uri.class, context)); + } + + public FileDescriptorStringLoader(ModelLoader uriLoader) { + super(uriLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorUriLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorUriLoader.java new file mode 100755 index 0000000..4034737 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/file_descriptor/FileDescriptorUriLoader.java @@ -0,0 +1,56 @@ +package com.example.bumptech.glide.load.model.file_descriptor; + +import android.content.Context; +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.data.FileDescriptorAssetPathFetcher; +import com.example.bumptech.glide.load.data.FileDescriptorLocalUriFetcher; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.UriLoader; + + +/** + * A {@link ModelLoader} For translating {@link Uri} models for local uris into {@link ParcelFileDescriptor} data. + */ +public class FileDescriptorUriLoader extends UriLoader implements FileDescriptorModelLoader { + + /** + * The default factory for {@link FileDescriptorUriLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new FileDescriptorUriLoader(context, factories.buildModelLoader(GlideUrl.class, + ParcelFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public FileDescriptorUriLoader(Context context) { + this(context, Glide.buildFileDescriptorModelLoader(GlideUrl.class, context)); + } + + public FileDescriptorUriLoader(Context context, ModelLoader urlLoader) { + super(context, urlLoader); + } + + @Override + protected DataFetcher getLocalUriFetcher(Context context, Uri uri) { + return new FileDescriptorLocalUriFetcher(context, uri); + } + + @Override + protected DataFetcher getAssetPathFetcher(Context context, String assetPath) { + return new FileDescriptorAssetPathFetcher(context.getApplicationContext().getAssets(), assetPath); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/BaseGlideUrlLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/BaseGlideUrlLoader.java new file mode 100755 index 0000000..619ee96 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/BaseGlideUrlLoader.java @@ -0,0 +1,87 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.text.TextUtils; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.Headers; +import com.example.bumptech.glide.load.model.ModelCache; +import com.example.bumptech.glide.load.model.ModelLoader; + +import java.io.InputStream; + +/** + * A base class for loading images over http/https. Can be subclassed for use with any model that can be translated + * in to {@link InputStream} data. + * + * @param The type of the model. + */ +public abstract class BaseGlideUrlLoader implements StreamModelLoader { + private final ModelLoader concreteLoader; + private final ModelCache modelCache; + + public BaseGlideUrlLoader(Context context) { + this(context, null); + } + + public BaseGlideUrlLoader(Context context, ModelCache modelCache) { + this(Glide.buildModelLoader(GlideUrl.class, InputStream.class, context), modelCache); + } + + public BaseGlideUrlLoader(ModelLoader concreteLoader) { + this(concreteLoader, null); + } + + public BaseGlideUrlLoader(ModelLoader concreteLoader, ModelCache modelCache) { + this.concreteLoader = concreteLoader; + this.modelCache = modelCache; + } + + @Override + public DataFetcher getResourceFetcher(T model, int width, int height) { + GlideUrl result = null; + if (modelCache != null) { + result = modelCache.get(model, width, height); + } + + if (result == null) { + String stringURL = getUrl(model, width, height); + if (TextUtils.isEmpty(stringURL)) { + return null; + } + + result = new GlideUrl(stringURL, getHeaders(model, width, height)); + + if (modelCache != null) { + modelCache.put(model, width, height, result); + } + } + + return concreteLoader.getResourceFetcher(result, width, height); + } + + /** + * Get a valid url http:// or https:// for the given model and dimensions as a string. + * + * @param model The model. + * @param width The width in pixels of the view/target the image will be loaded into. + * @param height The height in pixels of the view/target the image will be loaded into. + * @return The String url. + */ + protected abstract String getUrl(T model, int width, int height); + + /** + * Get the headers for the given model and dimensions as a map of strings to sets of strings. + * + * @param model The model. + * @param width The width in pixels of the view/target the image will be loaded into. + * @param height The height in pixels of the view/target the image will be loaded into. + * @return The Headers object containing the headers, or null if no headers should be added. + */ + protected Headers getHeaders(T model, int width, int height) { + return Headers.DEFAULT; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java new file mode 100755 index 0000000..8479c7a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java @@ -0,0 +1,62 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; + + +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.data.HttpUrlFetcher; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.ModelCache; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; + +import java.io.InputStream; + +/** + * An {@link ModelLoader} for translating {@link GlideUrl} + * (http/https URLS) into {@link InputStream} data. + */ +public class HttpUrlGlideUrlLoader implements ModelLoader { + + private final ModelCache modelCache; + + /** + * The default factory for {@link HttpUrlGlideUrlLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + private final ModelCache modelCache = new ModelCache(500); + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new HttpUrlGlideUrlLoader(modelCache); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public HttpUrlGlideUrlLoader() { + this(null); + } + + public HttpUrlGlideUrlLoader(ModelCache modelCache) { + this.modelCache = modelCache; + } + + @Override + public DataFetcher getResourceFetcher(GlideUrl model, int width, int height) { + // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time spent parsing urls. + GlideUrl url = model; + if (modelCache != null) { + url = modelCache.get(model, 0, 0); + if (url == null) { + modelCache.put(model, 0, 0, model); + url = model; + } + } + return new HttpUrlFetcher(url); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/MediaStoreStreamLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/MediaStoreStreamLoader.java new file mode 100755 index 0000000..662975d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/MediaStoreStreamLoader.java @@ -0,0 +1,35 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.net.Uri; + + +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.data.MediaStoreThumbFetcher; +import com.example.bumptech.glide.load.model.ModelLoader; + +import java.io.InputStream; + +/** + * An {@link ModelLoader} that can use media store uris to open pre-generated thumbnails + * from the media store using {@link android.provider.MediaStore.Images.Thumbnails} and + * {@link android.provider.MediaStore.Video.Thumbnails} if the requested size is less than or equal to the media store + * thumbnail size. If the given uri is not a media store uri or if the desired dimensions are too large, + * it falls back to the wrapped {@link ModelLoader} to load the + * {@link InputStream} data. + */ +public class MediaStoreStreamLoader implements ModelLoader { + private final Context context; + private final ModelLoader uriLoader; + + public MediaStoreStreamLoader(Context context, ModelLoader uriLoader) { + this.context = context; + this.uriLoader = uriLoader; + } + + @Override + public DataFetcher getResourceFetcher(Uri model, int width, int height) { + return new MediaStoreThumbFetcher(context, model, uriLoader.getResourceFetcher(model, width, height), width, + height); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamByteArrayLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamByteArrayLoader.java new file mode 100755 index 0000000..5b729b5 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamByteArrayLoader.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; + + +import com.example.bumptech.glide.load.data.ByteArrayFetcher; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; + +import java.io.InputStream; + +/** + * A base class to convert byte arrays to input streams so they can be decoded. This class is abstract because there is + * no simple/quick way to generate an id from the bytes themselves, so subclass must include an id. + */ +public class StreamByteArrayLoader implements StreamModelLoader { + private final String id; + + public StreamByteArrayLoader() { + this(""); + } + + /** + * @deprecated Use {@link com.bumptech.glide.GenericRequestBuilder#signature(com.bumptech.glide.load.Key)} + * and the empty constructor instead. Scheduled to be removed in Glide 4.0. + */ + @Deprecated + public StreamByteArrayLoader(String id) { + this.id = id; + } + + @Override + public DataFetcher getResourceFetcher(byte[] model, int width, int height) { + return new ByteArrayFetcher(model, id); + } + + /** + * Factory for {@link StreamByteArrayLoader}. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamByteArrayLoader(); + } + + @Override + public void teardown() { + // Do nothing. + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamFileLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamFileLoader.java new file mode 100755 index 0000000..983d157 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamFileLoader.java @@ -0,0 +1,44 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.net.Uri; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.FileLoader; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; + +import java.io.File; +import java.io.InputStream; + +/** + * A {@link ModelLoader} For translating {@link File} models for local uris into {@link InputStream} data. + */ +public class StreamFileLoader extends FileLoader implements StreamModelLoader { + + /** + * The default factory for {@link StreamFileLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamFileLoader(factories.buildModelLoader(Uri.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public StreamFileLoader(Context context) { + this(Glide.buildStreamModelLoader(Uri.class, context)); + } + + public StreamFileLoader(ModelLoader uriLoader) { + super(uriLoader); + } + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamModelLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamModelLoader.java new file mode 100755 index 0000000..d4d1b4e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamModelLoader.java @@ -0,0 +1,15 @@ +package com.example.bumptech.glide.load.model.stream; + + +import com.example.bumptech.glide.load.model.ModelLoader; + +import java.io.InputStream; + +/** + * A base class for {@link ModelLoader}s that translate models into {@link InputStream}s. + * + * @param The type of the model that will be translated into an {@link InputStream}. + */ +public interface StreamModelLoader extends ModelLoader { + // specializing the generic arguments +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamResourceLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamResourceLoader.java new file mode 100755 index 0000000..cb3fa8d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamResourceLoader.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.net.Uri; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.ResourceLoader; + +import java.io.InputStream; + +/** + * A {@link ModelLoader} For translating android resource id models for local uris into {@link InputStream} data. + */ +public class StreamResourceLoader extends ResourceLoader implements StreamModelLoader { + + /** + * The default factory for {@link StreamResourceLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamResourceLoader(context, factories.buildModelLoader(Uri.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public StreamResourceLoader(Context context) { + this(context, Glide.buildStreamModelLoader(Uri.class, context)); + } + + public StreamResourceLoader(Context context, ModelLoader uriLoader) { + super(context, uriLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamStringLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamStringLoader.java new file mode 100755 index 0000000..eacdc10 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamStringLoader.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.net.Uri; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.StringLoader; + +import java.io.InputStream; + +/** + * A {@link ModelLoader} for translating {@link String} models, such as file paths or remote urls, into + * {@link InputStream} data. + */ +public class StreamStringLoader extends StringLoader implements StreamModelLoader { + + /** + * The default factory for {@link StreamStringLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamStringLoader(factories.buildModelLoader(Uri.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public StreamStringLoader(Context context) { + this(Glide.buildStreamModelLoader(Uri.class, context)); + } + + public StreamStringLoader(ModelLoader uriLoader) { + super(uriLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUriLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUriLoader.java new file mode 100755 index 0000000..62d7a4a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUriLoader.java @@ -0,0 +1,59 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; +import android.net.Uri; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.data.StreamAssetPathFetcher; +import com.example.bumptech.glide.load.data.StreamLocalUriFetcher; +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.UriLoader; + +import java.io.InputStream; + +/** + * A {@link ModelLoader} for translating uri models into {@link InputStream} data. Capable of handling 'http', + * 'https', 'android.resource', 'content', and 'file' schemes. Unsupported schemes will throw an exception in + * {@link #getResourceFetcher(Uri, int, int)}. + */ +public class StreamUriLoader extends UriLoader implements StreamModelLoader { + + /** + * THe default factory for {@link StreamUriLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamUriLoader(context, factories.buildModelLoader(GlideUrl.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public StreamUriLoader(Context context) { + this(context, Glide.buildStreamModelLoader(GlideUrl.class, context)); + } + + public StreamUriLoader(Context context, ModelLoader urlLoader) { + super(context, urlLoader); + } + + @Override + protected DataFetcher getLocalUriFetcher(Context context, Uri uri) { + return new StreamLocalUriFetcher(context, uri); + } + + @Override + protected DataFetcher getAssetPathFetcher(Context context, String assetPath) { + return new StreamAssetPathFetcher(context.getApplicationContext().getAssets(), assetPath); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUrlLoader.java b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUrlLoader.java new file mode 100755 index 0000000..dfe5cf6 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/model/stream/StreamUrlLoader.java @@ -0,0 +1,40 @@ +package com.example.bumptech.glide.load.model.stream; + +import android.content.Context; + + +import com.example.bumptech.glide.load.model.GenericLoaderFactory; +import com.example.bumptech.glide.load.model.GlideUrl; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.model.ModelLoaderFactory; +import com.example.bumptech.glide.load.model.UrlLoader; + +import java.io.InputStream; +import java.net.URL; + +/** + * A wrapper class that translates {@link URL} objects into {@link GlideUrl} + * objects and then uses the wrapped {@link ModelLoader} for + * {@link GlideUrl}s to load the {@link InputStream} data. + */ +public class StreamUrlLoader extends UrlLoader { + + /** + * The default factory for {@link StreamUrlLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new StreamUrlLoader(factories.buildModelLoader(GlideUrl.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public StreamUrlLoader(ModelLoader glideUrlLoader) { + super(glideUrlLoader); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/NullDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/NullDecoder.java new file mode 100755 index 0000000..f774df1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/NullDecoder.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.resource; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A simple {@link ResourceDecoder} that always returns null. + * + * @param The type of the data that will be ignored by this class. + * @param The type of the decoded resource that will always be null. + */ +public class NullDecoder implements ResourceDecoder { + private static final NullDecoder NULL_DECODER = new NullDecoder(); + + /** + * Returns an instance of the NullDecoder for the given types. + * + * @param The data type. + * @param The resource type. + */ + @SuppressWarnings("unchecked") + public static NullDecoder get() { + return (NullDecoder) NULL_DECODER; + } + + @Override + public Resource decode(T source, int width, int height) { + return null; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/NullEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/NullEncoder.java new file mode 100755 index 0000000..6602fa6 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/NullEncoder.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.resource; + + +import com.example.bumptech.glide.load.Encoder; + +import java.io.OutputStream; + +/** + * A simple {@link Encoder} that never writes data. + * + * @param type discarded by this Encoder + */ +public class NullEncoder implements Encoder { + private static final NullEncoder NULL_ENCODER = new NullEncoder(); + + /** + * Returns an Encoder for the given data type. + * + * @param The type of data to be written (or not in this case). + */ + @SuppressWarnings("unchecked") + public static Encoder get() { + return (Encoder) NULL_ENCODER; + + } + + @Override + public boolean encode(T data, OutputStream os) { + return false; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/NullResourceEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/NullResourceEncoder.java new file mode 100755 index 0000000..0d3bd03 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/NullResourceEncoder.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.resource; + + +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.Resource; + +import java.io.OutputStream; + +/** + * A simple {@link ResourceEncoder} that never writes data. + * + * @param The type of the resource that will always fail to be encoded. + */ +public class NullResourceEncoder implements ResourceEncoder { + private static final NullResourceEncoder NULL_ENCODER = new NullResourceEncoder(); + + /** + * Returns a NullResourceEncoder for the given type. + * + * @param The type of data to be written (or in this case not written). + */ + @SuppressWarnings("unchecked") + public static NullResourceEncoder get() { + return (NullResourceEncoder) NULL_ENCODER; + } + + @Override + public boolean encode(Resource data, OutputStream os) { + return false; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/SimpleResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/SimpleResource.java new file mode 100755 index 0000000..d5b9b21 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/SimpleResource.java @@ -0,0 +1,37 @@ +package com.example.bumptech.glide.load.resource; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * Simple wrapper for an arbitrary object which helps to satisfy some of the glide engine's contracts. + * Suggested usages only include resource object which don't have size and cannot be recycled/closed. + * + * @param type of the wrapped resource + */ +// TODO: there isn't much point in caching these... +public class SimpleResource implements Resource { + protected final T data; + + public SimpleResource(T data) { + if (data == null) { + throw new NullPointerException("Data must not be null"); + } + this.data = data; + } + + @Override + public final T get() { + return data; + } + + @Override + public final int getSize() { + return 1; + } + + @Override + public void recycle() { + // no op + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/UnitTransformation.java b/core/src/main/java/com/example/bumptech/glide/load/resource/UnitTransformation.java new file mode 100755 index 0000000..6836f9b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/UnitTransformation.java @@ -0,0 +1,34 @@ +package com.example.bumptech.glide.load.resource; + + +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A noop Transformation that simply returns the given resource. + * + * @param The type of the resource that will always be returned unmodified. + */ +public class UnitTransformation implements Transformation { + private static final Transformation TRANSFORMATION = new UnitTransformation(); + + /** + * Returns a UnitTransformation for the given type. + * + * @param The type of the resource to be transformed. + */ + @SuppressWarnings("unchecked") + public static UnitTransformation get() { + return (UnitTransformation) TRANSFORMATION; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + return resource; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDecoder.java new file mode 100755 index 0000000..ff5c886 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDecoder.java @@ -0,0 +1,40 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + + +/** + * A bitmap decoder for a given resource type. + * + * @param The type of resource this decoder can decode a {@link Bitmap} from. + */ +public interface BitmapDecoder { + /** + * Returns a decoded bitmap for a given resource and target dimensions. + * + * @param resource The resource to decode, managed by the caller, no need to clean it up. + * @param bitmapPool A bitmap pool that can be used to reuse bitmaps during the load. Any bitmaps created or + * obtained from the pool other than the bitmap returned by this method should be returned to the + * pool. + * @param outWidth The target width for the returned bitmap (need not match exactly). + * @param outHeight The target height for the returned bitmap (need not match exactly). + * @param decodeFormat The desired configuration for the returned bitmap. + */ + Bitmap decode(T resource, BitmapPool bitmapPool, int outWidth, int outHeight, DecodeFormat decodeFormat) + throws Exception; + + /** + * Returns some unique String id that distinguishes this decoder from any other decoder. + * + *

+ * This method can return the empty string if for all practical purposes it applies no transformations to the + * data while loading the resource. For {@link Bitmap}s this would mean at a minimum doing no + * downsampling and also probably always producing {@link Bitmap}s with + * {@link Bitmap.Config#ARGB_8888} as their config. + *

+ */ + String getId(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDrawableResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDrawableResource.java new file mode 100755 index 0000000..1094bc1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapDrawableResource.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.drawable.BitmapDrawable; + +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.drawable.DrawableResource; +import com.example.bumptech.glide.util.Util; + + +/** + * A {@link com.bumptech.glide.load.engine.Resource} that wraps an {@link BitmapDrawable} + *

+ * This class ensures that every call to {@link #get()}} always returns a new + * {@link BitmapDrawable} to avoid rendering issues if used in multiple views and + * is also responsible for returning the underlying {@link android.graphics.Bitmap} to the given + * {@link BitmapPool} when the resource is recycled. + *

+ */ +public class BitmapDrawableResource extends DrawableResource { + private final BitmapPool bitmapPool; + + public BitmapDrawableResource(BitmapDrawable drawable, BitmapPool bitmapPool) { + super(drawable); + this.bitmapPool = bitmapPool; + } + + @Override + public int getSize() { + return Util.getBitmapByteSize(drawable.getBitmap()); + } + + @Override + public void recycle() { + bitmapPool.put(drawable.getBitmap()); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapEncoder.java new file mode 100755 index 0000000..16d28e1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapEncoder.java @@ -0,0 +1,70 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.util.Log; + + +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.util.LogTime; +import com.example.bumptech.glide.util.Util; + +import java.io.OutputStream; + +/** + * An {@link ResourceEncoder} that writes {@link Bitmap}s to + * {@link OutputStream}s. + * + *

+ * {@link Bitmap}s that return true from {@link Bitmap#hasAlpha()}} are written + * using {@link Bitmap.CompressFormat#PNG} to preserve alpha and all other bitmaps are written + * using {@link Bitmap.CompressFormat#JPEG}. + *

+ * + * @see Bitmap#compress(Bitmap.CompressFormat, int, OutputStream) + */ +public class BitmapEncoder implements ResourceEncoder { + private static final String TAG = "BitmapEncoder"; + private static final int DEFAULT_COMPRESSION_QUALITY = 90; + private Bitmap.CompressFormat compressFormat; + private int quality; + + public BitmapEncoder() { + this(null, DEFAULT_COMPRESSION_QUALITY); + } + + public BitmapEncoder(Bitmap.CompressFormat compressFormat, int quality) { + this.compressFormat = compressFormat; + this.quality = quality; + } + + @Override + public boolean encode(Resource resource, OutputStream os) { + final Bitmap bitmap = resource.get(); + + long start = LogTime.getLogTime(); + Bitmap.CompressFormat format = getFormat(bitmap); + bitmap.compress(format, quality, os); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Compressed with type: " + format + " of size " + Util.getBitmapByteSize(bitmap) + " in " + + LogTime.getElapsedMillis(start)); + } + return true; + } + + @Override + public String getId() { + return "BitmapEncoder.com.bumptech.glide.load.resource.bitmap"; + } + + private Bitmap.CompressFormat getFormat(Bitmap bitmap) { + if (compressFormat != null) { + return compressFormat; + } else if (bitmap.hasAlpha()) { + return Bitmap.CompressFormat.PNG; + } else { + return Bitmap.CompressFormat.JPEG; + } + } + +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapResource.java new file mode 100755 index 0000000..57e1e3d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapResource.java @@ -0,0 +1,59 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.util.Util; + + +/** + * A resource wrapping a {@link Bitmap} object. + */ +public class BitmapResource implements Resource { + private final Bitmap bitmap; + private final BitmapPool bitmapPool; + + /** + * Returns a new {@link BitmapResource} wrapping the given {@link Bitmap} if the Bitmap is non-null or null if the + * given Bitmap is null. + * + * @param bitmap A Bitmap. + * @param bitmapPool A non-null {@link BitmapPool}. + */ + public static BitmapResource obtain(Bitmap bitmap, BitmapPool bitmapPool) { + if (bitmap == null) { + return null; + } else { + return new BitmapResource(bitmap, bitmapPool); + } + } + + public BitmapResource(Bitmap bitmap, BitmapPool bitmapPool) { + if (bitmap == null) { + throw new NullPointerException("Bitmap must not be null"); + } + if (bitmapPool == null) { + throw new NullPointerException("BitmapPool must not be null"); + } + this.bitmap = bitmap; + this.bitmapPool = bitmapPool; + } + + @Override + public Bitmap get() { + return bitmap; + } + + @Override + public int getSize() { + return Util.getBitmapByteSize(bitmap); + } + + @Override + public void recycle() { + if (!bitmapPool.put(bitmap)) { + bitmap.recycle(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapTransformation.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapTransformation.java new file mode 100755 index 0000000..191619f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/BitmapTransformation.java @@ -0,0 +1,95 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.request.target.Target; +import com.example.bumptech.glide.util.Util; + + +/** + * A simple {@link Transformation} for transforming {@link Bitmap}s that + * abstracts away dealing with {@link Resource} objects for subclasses. + * + * Use cases will look something like this: + *
+ * 
+ * public class FillSpace extends BaseBitmapTransformation {
+ *     {@literal @Override}
+ *     public Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
+ *         if (toTransform.getWidth() == outWidth && toTransform.getHeight() == outHeight) {
+ *             return toTransform;
+ *         }
+ *
+ *         return Bitmap.createScaledBitmap(toTransform, outWidth, outHeight, true);
+ *     }
+ * }
+ * 
+ * 
+ */ +public abstract class BitmapTransformation implements Transformation { + + private BitmapPool bitmapPool; + + public BitmapTransformation(Context context) { + this(Glide.get(context).getBitmapPool()); + } + + public BitmapTransformation(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + } + + @Override + public final Resource transform(Resource resource, int outWidth, int outHeight) { + if (!Util.isValidDimensions(outWidth, outHeight)) { + throw new IllegalArgumentException("Cannot apply transformation on width: " + outWidth + " or height: " + + outHeight + " less than or equal to zero and not Target.SIZE_ORIGINAL"); + } + Bitmap toTransform = resource.get(); + int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth; + int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight; + Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight); + + final Resource result; + if (toTransform.equals(transformed)) { + result = resource; + } else { + result = BitmapResource.obtain(transformed, bitmapPool); + } + + return result; + } + + /** + * Transforms the given {@link Bitmap} based on the given dimensions and returns the transformed + * result. + * + *

+ * The provided Bitmap, toTransform, should not be recycled or returned to the pool. Glide will automatically + * recycle and/or reuse toTransform if the transformation returns a different Bitmap. Similarly implementations + * should never recycle or return Bitmaps that are returned as the result of this method. Recycling or returning + * the provided and/or the returned Bitmap to the pool will lead to a variety of runtime exceptions and drawing + * errors. See #408 for an example. If the implementation obtains and discards intermediate Bitmaps, they may + * safely be returned to the BitmapPool and/or recycled. + *

+ * + *

+ * outWidth and outHeight will never be {@link Target#SIZE_ORIGINAL}, this + * class converts them to be the size of the Bitmap we're going to transform before calling this method. + *

+ * + * @param pool A {@link BitmapPool} that can be used to obtain and + * return intermediate {@link Bitmap}s used in this transformation. For every + * {@link Bitmap} obtained from the pool during this transformation, a + * {@link Bitmap} must also be returned. + * @param toTransform The {@link Bitmap} to transform. + * @param outWidth The ideal width of the transformed bitmap (the transformed width does not need to match exactly). + * @param outHeight The ideal height of the transformed bitmap (the transformed heightdoes not need to match + * exactly). + */ + protected abstract Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/CenterCrop.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/CenterCrop.java new file mode 100755 index 0000000..18d7426 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/CenterCrop.java @@ -0,0 +1,42 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + + +/** + * Scale the image so that either the width of the image matches the given width and the height of the image is + * greater than the given height or vice versa, and then crop the larger dimension to match the given dimension. + * + * Does not maintain the image's aspect ratio + */ +public class CenterCrop extends BitmapTransformation { + + public CenterCrop(Context context) { + super(context); + } + + public CenterCrop(BitmapPool bitmapPool) { + super(bitmapPool); + } + + // Bitmap doesn't implement equals, so == and .equals are equivalent here. + @SuppressWarnings("PMD.CompareObjectsWithEquals") + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { + final Bitmap toReuse = pool.get(outWidth, outHeight, toTransform.getConfig() != null + ? toTransform.getConfig() : Bitmap.Config.ARGB_8888); + Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight); + if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) { + toReuse.recycle(); + } + return transformed; + } + + @Override + public String getId() { + return "CenterCrop.com.bumptech.glide.load.resource.bitmap"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/Downsampler.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/Downsampler.java new file mode 100755 index 0000000..8da02a9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/Downsampler.java @@ -0,0 +1,391 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.util.Log; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.request.target.Target; +import com.example.bumptech.glide.util.ByteArrayPool; +import com.example.bumptech.glide.util.ExceptionCatchingInputStream; +import com.example.bumptech.glide.util.MarkEnforcingInputStream; +import com.example.bumptech.glide.util.Util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.EnumSet; +import java.util.Queue; +import java.util.Set; + +/** + * A base class with methods for loading and decoding images from InputStreams. + */ +public abstract class Downsampler implements BitmapDecoder { + private static final String TAG = "Downsampler"; + + private static final Set TYPES_THAT_USE_POOL = EnumSet.of( + ImageHeaderParser.ImageType.JPEG, ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG); + + private static final Queue OPTIONS_QUEUE = Util.createQueue(0); + + /** + * Load and scale the image uniformly (maintaining the image's aspect ratio) so that the smallest edge of the + * image will be between 1x and 2x the requested size. The larger edge has no maximum size. + */ + public static final Downsampler AT_LEAST = new Downsampler() { + @Override + protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) { + return Math.min(inHeight / outHeight, inWidth / outWidth); + } + + @Override + public String getId() { + return "AT_LEAST.com.bumptech.glide.load.data.bitmap"; + } + }; + + /** + * Load and scale the image uniformly (maintaining the image's aspect ratio) so that largest edge of the image + * will be between 1/2x and 1x of the requested size. The smaller edge has no minimum size. + */ + public static final Downsampler AT_MOST = new Downsampler() { + @Override + protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) { + int maxIntegerFactor = (int) Math.ceil(Math.max(inHeight / (float) outHeight, + inWidth / (float) outWidth)); + int lesserOrEqualSampleSize = Math.max(1, Integer.highestOneBit(maxIntegerFactor)); + return lesserOrEqualSampleSize << (lesserOrEqualSampleSize < maxIntegerFactor ? 1 : 0); + } + + @Override + public String getId() { + return "AT_MOST.com.bumptech.glide.load.data.bitmap"; + } + }; + + /** + * Load the image at its original size. + */ + public static final Downsampler NONE = new Downsampler() { + @Override + protected int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight) { + return 0; + } + + @Override + public String getId() { + return "NONE.com.bumptech.glide.load.data.bitmap"; + } + }; + + // 5MB. This is the max image header size we can handle, we preallocate a much smaller buffer but will resize up to + // this amount if necessary. + private static final int MARK_POSITION = 5 * 1024 * 1024; + + + /** + * Load the image for the given InputStream. If a recycled Bitmap whose dimensions exactly match those of the image + * for the given InputStream is available, the operation is much less expensive in terms of memory. + * + *

+ * Note - this method will throw an exception of a Bitmap with dimensions not matching + * those of the image for the given InputStream is provided. + *

+ * + * @param is An {@link InputStream} to the data for the image. + * @param pool A pool of recycled bitmaps. + * @param outWidth The width the final image should be close to. + * @param outHeight The height the final image should be close to. + * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is not null. + */ + @SuppressWarnings("resource") + // see BitmapDecoder.decode + @Override + public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) { + final ByteArrayPool byteArrayPool = ByteArrayPool.get(); + final byte[] bytesForOptions = byteArrayPool.getBytes(); + final byte[] bytesForStream = byteArrayPool.getBytes(); + final BitmapFactory.Options options = getDefaultOptions(); + + // Use to fix the mark limit to avoid allocating buffers that fit entire images. + RecyclableBufferedInputStream bufferedStream = new RecyclableBufferedInputStream( + is, bytesForStream); + // Use to retrieve exceptions thrown while reading. + // TODO(#126): when the framework no longer returns partially decoded Bitmaps or provides a way to determine + // if a Bitmap is partially decoded, consider removing. + ExceptionCatchingInputStream exceptionStream = + ExceptionCatchingInputStream.obtain(bufferedStream); + // Use to read data. + // Ensures that we can always reset after reading an image header so that we can still attempt to decode the + // full image even when the header decode fails and/or overflows our read buffer. See #283. + MarkEnforcingInputStream invalidatingStream = new MarkEnforcingInputStream(exceptionStream); + try { + exceptionStream.mark(MARK_POSITION); + int orientation = 0; + try { + orientation = new ImageHeaderParser(exceptionStream).getOrientation(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot determine the image orientation from header", e); + } + } finally { + try { + exceptionStream.reset(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot reset the input stream", e); + } + } + } + + options.inTempStorage = bytesForOptions; + + final int[] inDimens = getDimensions(invalidatingStream, bufferedStream, options); + final int inWidth = inDimens[0]; + final int inHeight = inDimens[1]; + + final int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); + final int sampleSize = getRoundedSampleSize(degreesToRotate, inWidth, inHeight, outWidth, outHeight); + + final Bitmap downsampled = + downsampleWithSize(invalidatingStream, bufferedStream, options, pool, inWidth, inHeight, sampleSize, + decodeFormat); + + // BitmapFactory swallows exceptions during decodes and in some cases when inBitmap is non null, may catch + // and log a stack trace but still return a non null bitmap. To avoid displaying partially decoded bitmaps, + // we catch exceptions reading from the stream in our ExceptionCatchingInputStream and throw them here. + final Exception streamException = exceptionStream.getException(); + if (streamException != null) { + throw new RuntimeException(streamException); + } + + Bitmap rotated = null; + if (downsampled != null) { + rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation); + + if (!downsampled.equals(rotated) && !pool.put(downsampled)) { + downsampled.recycle(); + } + } + + return rotated; + } finally { + byteArrayPool.releaseBytes(bytesForOptions); + byteArrayPool.releaseBytes(bytesForStream); + exceptionStream.release(); + releaseOptions(options); + } + } + + private int getRoundedSampleSize(int degreesToRotate, int inWidth, int inHeight, int outWidth, int outHeight) { + int targetHeight = outHeight == Target.SIZE_ORIGINAL ? inHeight : outHeight; + int targetWidth = outWidth == Target.SIZE_ORIGINAL ? inWidth : outWidth; + + final int exactSampleSize; + if (degreesToRotate == 90 || degreesToRotate == 270) { + // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image width is + // decreased to near our target's height and the image height is decreased to near our target width. + //noinspection SuspiciousNameCombination + exactSampleSize = getSampleSize(inHeight, inWidth, targetWidth, targetHeight); + } else { + exactSampleSize = getSampleSize(inWidth, inHeight, targetWidth, targetHeight); + } + + // BitmapFactory only accepts powers of 2, so it will round down to the nearest power of two that is less than + // or equal to the sample size we provide. Because we need to estimate the final image width and height to + // re-use Bitmaps, we mirror BitmapFactory's calculation here. For bug, see issue #224. For algorithm see + // http://stackoverflow.com/a/17379704/800716. + final int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize); + + // Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code than 0. + return Math.max(1, powerOfTwoSampleSize); + } + + private Bitmap downsampleWithSize(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream, + BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize, + DecodeFormat decodeFormat) { + // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding. + Bitmap.Config config = getConfig(is, decodeFormat); + options.inSampleSize = sampleSize; + options.inPreferredConfig = config; + if ((options.inSampleSize == 1 || Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) && shouldUsePool(is)) { + int targetWidth = (int) Math.ceil(inWidth / (double) sampleSize); + int targetHeight = (int) Math.ceil(inHeight / (double) sampleSize); + // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe. + setInBitmap(options, pool.getDirty(targetWidth, targetHeight, config)); + } + return decodeStream(is, bufferedStream, options); + } + + private static boolean shouldUsePool(InputStream is) { + // On KitKat+, any bitmap can be used to decode any other bitmap. + if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) { + return true; + } + + is.mark(1024); + try { + final ImageHeaderParser.ImageType type = new ImageHeaderParser(is).getType(); + // cannot reuse bitmaps when decoding images that are not PNG or JPG. + // look at : https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ + return TYPES_THAT_USE_POOL.contains(type); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot determine the image type from header", e); + } + } finally { + try { + is.reset(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot reset the input stream", e); + } + } + } + return false; + } + + @SuppressWarnings("deprecation") + private static Bitmap.Config getConfig(InputStream is, DecodeFormat format) { + // Changing configs can cause skewing on 4.1, see issue #128. + if (format == DecodeFormat.ALWAYS_ARGB_8888 || format == DecodeFormat.PREFER_ARGB_8888 + || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) { + return Bitmap.Config.ARGB_8888; + } + + boolean hasAlpha = false; + // We probably only need 25, but this is safer (particularly since the buffer size is > 1024). + is.mark(1024); + try { + hasAlpha = new ImageHeaderParser(is).hasAlpha(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot determine whether the image has alpha or not from header for format " + format, e); + } + } finally { + try { + is.reset(); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Cannot reset the input stream", e); + } + } + } + + return hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; + } + + /** + * Determine the amount of downsampling to use for a load given the dimensions of the image to be downsampled and + * the dimensions of the view/target the image will be displayed in. + * + * @see BitmapFactory.Options#inSampleSize + * + * @param inWidth The width in pixels of the image to be downsampled. + * @param inHeight The height in piexels of the image to be downsampled. + * @param outWidth The width in pixels of the view/target the image will be displayed in. + * @param outHeight The height in pixels of the view/target the imag will be displayed in. + * @return An integer to pass in to {@link BitmapFactory#decodeStream(InputStream, android.graphics.Rect, + * BitmapFactory.Options)}. + */ + protected abstract int getSampleSize(int inWidth, int inHeight, int outWidth, int outHeight); + + /** + * A method for getting the dimensions of an image from the given InputStream. + * + * @param is The InputStream representing the image. + * @param options The options to pass to + * {@link BitmapFactory#decodeStream(InputStream, android.graphics.Rect, + * BitmapFactory.Options)}. + * @return an array containing the dimensions of the image in the form {width, height}. + */ + public int[] getDimensions(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream, + BitmapFactory.Options options) { + options.inJustDecodeBounds = true; + decodeStream(is, bufferedStream, options); + options.inJustDecodeBounds = false; + return new int[] { options.outWidth, options.outHeight }; + } + + private static Bitmap decodeStream(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream, + BitmapFactory.Options options) { + if (options.inJustDecodeBounds) { + // This is large, but jpeg headers are not size bounded so we need something large enough to minimize + // the possibility of not being able to fit enough of the header in the buffer to get the image size so + // that we don't fail to load images. The BufferedInputStream will create a new buffer of 2x the + // original size each time we use up the buffer space without passing the mark so this is a maximum + // bound on the buffer size, not a default. Most of the time we won't go past our pre-allocated 16kb. + is.mark(MARK_POSITION); + } else { + // Once we've read the image header, we no longer need to allow the buffer to expand in size. To avoid + // unnecessary allocations reading image data, we fix the mark limit so that it is no larger than our + // current buffer size here. See issue #225. + bufferedStream.fixMarkLimit(); + } + + final Bitmap result = BitmapFactory.decodeStream(is, null, options); + + try { + if (options.inJustDecodeBounds) { + is.reset(); + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "Exception loading inDecodeBounds=" + options.inJustDecodeBounds + + " sample=" + options.inSampleSize, e); + } + } + + return result; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void setInBitmap(BitmapFactory.Options options, Bitmap recycled) { + if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) { + options.inBitmap = recycled; + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static synchronized BitmapFactory.Options getDefaultOptions() { + BitmapFactory.Options decodeBitmapOptions; + synchronized (OPTIONS_QUEUE) { + decodeBitmapOptions = OPTIONS_QUEUE.poll(); + } + if (decodeBitmapOptions == null) { + decodeBitmapOptions = new BitmapFactory.Options(); + resetOptions(decodeBitmapOptions); + } + + return decodeBitmapOptions; + } + + private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) { + resetOptions(decodeBitmapOptions); + synchronized (OPTIONS_QUEUE) { + OPTIONS_QUEUE.offer(decodeBitmapOptions); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) { + decodeBitmapOptions.inTempStorage = null; + decodeBitmapOptions.inDither = false; + decodeBitmapOptions.inScaled = false; + decodeBitmapOptions.inSampleSize = 1; + decodeBitmapOptions.inPreferredConfig = null; + decodeBitmapOptions.inJustDecodeBounds = false; + decodeBitmapOptions.outWidth = 0; + decodeBitmapOptions.outHeight = 0; + decodeBitmapOptions.outMimeType = null; + + if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) { + decodeBitmapOptions.inBitmap = null; + decodeBitmapOptions.inMutable = true; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDataLoadProvider.java new file mode 100755 index 0000000..7068af2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDataLoadProvider.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.NullEncoder; +import com.example.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; + +/** + * An {@link DataLoadProvider} that provides classes for decoding and encoding + * {@link Bitmap}s from {@link ParcelFileDescriptor} data. + */ +public class FileDescriptorBitmapDataLoadProvider implements DataLoadProvider { + private final ResourceDecoder cacheDecoder; + private final FileDescriptorBitmapDecoder sourceDecoder; + private final BitmapEncoder encoder; + private final Encoder sourceEncoder; + + public FileDescriptorBitmapDataLoadProvider(BitmapPool bitmapPool, DecodeFormat decodeFormat) { + cacheDecoder = new FileToStreamDecoder(new StreamBitmapDecoder(bitmapPool, decodeFormat)); + sourceDecoder = new FileDescriptorBitmapDecoder(bitmapPool, decodeFormat); + encoder = new BitmapEncoder(); + sourceEncoder = NullEncoder.get(); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return sourceDecoder; + } + + @Override + public Encoder getSourceEncoder() { + return sourceEncoder; + } + + @Override + public ResourceEncoder getEncoder() { + return encoder; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDecoder.java new file mode 100755 index 0000000..1a2768e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FileDescriptorBitmapDecoder.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + +import java.io.IOException; + +/** + * An {@link ResourceDecoder} for decoding {@link Bitmap}s from + * {@link ParcelFileDescriptor} data. + */ +public class FileDescriptorBitmapDecoder implements ResourceDecoder { + private final VideoBitmapDecoder bitmapDecoder; + private final BitmapPool bitmapPool; + private DecodeFormat decodeFormat; + + public FileDescriptorBitmapDecoder(Context context) { + this(Glide.get(context).getBitmapPool(), DecodeFormat.DEFAULT); + } + + public FileDescriptorBitmapDecoder(Context context, DecodeFormat decodeFormat) { + this(Glide.get(context).getBitmapPool(), decodeFormat); + } + + public FileDescriptorBitmapDecoder(BitmapPool bitmapPool, DecodeFormat decodeFormat) { + this(new VideoBitmapDecoder(), bitmapPool, decodeFormat); + } + + public FileDescriptorBitmapDecoder(VideoBitmapDecoder bitmapDecoder, BitmapPool bitmapPool, + DecodeFormat decodeFormat) { + this.bitmapDecoder = bitmapDecoder; + this.bitmapPool = bitmapPool; + this.decodeFormat = decodeFormat; + } + + @Override + public Resource decode(ParcelFileDescriptor source, int width, int height) throws IOException { + Bitmap bitmap = bitmapDecoder.decode(source, bitmapPool, width, height, decodeFormat); + return BitmapResource.obtain(bitmap, bitmapPool); + } + + @Override + public String getId() { + return "FileDescriptorBitmapDecoder.com.bumptech.glide.load.data.bitmap"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FitCenter.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FitCenter.java new file mode 100755 index 0000000..a2f3f73 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/FitCenter.java @@ -0,0 +1,34 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + + +/** + * Scales the image uniformly (maintaining the image's aspect ratio) so that one of the dimensions of the image + * will be equal to the given dimension and the other will be less than the given dimension. + */ +public class FitCenter extends BitmapTransformation { + + public FitCenter(Context context) { + super(context); + } + + public FitCenter(BitmapPool bitmapPool) { + super(bitmapPool); + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { + return TransformationUtils.fitCenter(toTransform, pool, outWidth, outHeight); + } + + @Override + public String getId() { + return "FitCenter.com.bumptech.glide.load.resource.bitmap"; + } +} + + diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawable.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawable.java new file mode 100755 index 0000000..b7c9fff --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawable.java @@ -0,0 +1,193 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.view.Gravity; + +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; + + +/** + * A static {@link GlideDrawable} for displaying a single image. + */ +public class GlideBitmapDrawable extends GlideDrawable { + private final Rect destRect = new Rect(); + private int width; + private int height; + private boolean applyGravity; + private boolean mutated; + private BitmapState state; + + public GlideBitmapDrawable(Resources res, Bitmap bitmap) { + this(res, new BitmapState(bitmap)); + } + + GlideBitmapDrawable(Resources res, BitmapState state) { + if (state == null) { + throw new NullPointerException("BitmapState must not be null"); + } + + this.state = state; + final int targetDensity; + if (res != null) { + final int density = res.getDisplayMetrics().densityDpi; + targetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; + state.targetDensity = targetDensity; + } else { + targetDensity = state.targetDensity; + } + width = state.bitmap.getScaledWidth(targetDensity); + height = state.bitmap.getScaledHeight(targetDensity); + } + + @Override + public int getIntrinsicWidth() { + return width; + } + + @Override + public int getIntrinsicHeight() { + return height; + } + + @Override + public boolean isAnimated() { + return false; + } + + @Override + public void setLoopCount(int loopCount) { + // Do nothing. + } + + @Override + public void start() { + // Do nothing. + } + + @Override + public void stop() { + // Do nothing. + } + + @Override + public boolean isRunning() { + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + applyGravity = true; + } + + @Override + public Drawable.ConstantState getConstantState() { + return state; + } + + @Override + public void draw(Canvas canvas) { + if (applyGravity) { + Gravity.apply(BitmapState.GRAVITY, width, height, getBounds(), destRect); + applyGravity = false; + } + canvas.drawBitmap(state.bitmap, null, destRect, state.paint); + } + + @Override + public void setAlpha(int alpha) { + int currentAlpha = state.paint.getAlpha(); + if (currentAlpha != alpha) { + state.setAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + state.setColorFilter(colorFilter); + invalidateSelf(); + } + + @Override + public int getOpacity() { + Bitmap bm = state.bitmap; + return bm == null || bm.hasAlpha() || state.paint.getAlpha() < 255 + ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + @Override + public Drawable mutate() { + if (!mutated && super.mutate() == this) { + state = new BitmapState(state); + mutated = true; + } + return this; + } + + public Bitmap getBitmap() { + return state.bitmap; + } + + static class BitmapState extends Drawable.ConstantState { + private static final int DEFAULT_PAINT_FLAGS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; + private static final Paint DEFAULT_PAINT = new Paint(DEFAULT_PAINT_FLAGS); + private static final int GRAVITY = Gravity.FILL; + + final Bitmap bitmap; + + int targetDensity; + Paint paint = DEFAULT_PAINT; + + public BitmapState(Bitmap bitmap) { + this.bitmap = bitmap; + } + + + BitmapState(BitmapState other) { + this(other.bitmap); + targetDensity = other.targetDensity; + } + + void setColorFilter(ColorFilter colorFilter) { + mutatePaint(); + paint.setColorFilter(colorFilter); + } + + void setAlpha(int alpha) { + mutatePaint(); + paint.setAlpha(alpha); + } + + // We want to create a new Paint object so we can mutate it safely. + @SuppressWarnings("PMD.CompareObjectsWithEquals") + void mutatePaint() { + if (DEFAULT_PAINT == paint) { + paint = new Paint(DEFAULT_PAINT_FLAGS); + } + } + + @Override + public Drawable newDrawable() { + return new GlideBitmapDrawable(null, this); + } + + @Override + public Drawable newDrawable(Resources res) { + return new GlideBitmapDrawable(res, this); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawableResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawableResource.java new file mode 100755 index 0000000..7aa9dc3 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/GlideBitmapDrawableResource.java @@ -0,0 +1,27 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.drawable.DrawableResource; +import com.example.bumptech.glide.util.Util; + +/** + * A resource wrapper for {@link GlideBitmapDrawable}. + */ +public class GlideBitmapDrawableResource extends DrawableResource { + private final BitmapPool bitmapPool; + + public GlideBitmapDrawableResource(GlideBitmapDrawable drawable, BitmapPool bitmapPool) { + super(drawable); + this.bitmapPool = bitmapPool; + } + + @Override + public int getSize() { + return Util.getBitmapByteSize(drawable.getBitmap()); + } + + @Override + public void recycle() { + bitmapPool.put(drawable.getBitmap()); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageHeaderParser.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageHeaderParser.java new file mode 100755 index 0000000..6de88a1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageHeaderParser.java @@ -0,0 +1,382 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.GIF; +import static com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.JPEG; +import static com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG; +import static com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.PNG_A; +import static com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser.ImageType.UNKNOWN; + + +/** + * A class for parsing the exif orientation and other data from an image header. + */ +public class ImageHeaderParser { + private static final String TAG = "ImageHeaderParser"; + + /** + * The format of the image data including whether or not the image may include transparent pixels. + */ + public enum ImageType { + /** GIF type. */ + GIF(true), + /** JPG type. */ + JPEG(false), + /** PNG type with alpha. */ + PNG_A(true), + /** PNG type without alpha. */ + PNG(false), + /** Unrecognized type. */ + UNKNOWN(false); + private final boolean hasAlpha; + + ImageType(boolean hasAlpha) { + this.hasAlpha = hasAlpha; + } + + public boolean hasAlpha() { + return hasAlpha; + } + } + + private static final int GIF_HEADER = 0x474946; + private static final int PNG_HEADER = 0x89504E47; + private static final int EXIF_MAGIC_NUMBER = 0xFFD8; + // "MM". + private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D; + // "II". + private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949; + private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; + private static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES; + private static final int SEGMENT_SOS = 0xDA; + private static final int MARKER_EOI = 0xD9; + private static final int SEGMENT_START_ID = 0xFF; + private static final int EXIF_SEGMENT_TYPE = 0xE1; + private static final int ORIENTATION_TAG_TYPE = 0x0112; + private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; + + private final StreamReader streamReader; + + static { + byte[] bytes = new byte[0]; + try { + bytes = JPEG_EXIF_SEGMENT_PREAMBLE.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // Ignore. + } + JPEG_EXIF_SEGMENT_PREAMBLE_BYTES = bytes; + } + + public ImageHeaderParser(InputStream is) { + streamReader = new StreamReader(is); + } + + // 0xD0A3C68 -> = 3 ? PNG_A : PNG; + } + + // GIF from first 3 bytes. + if (firstFourBytes >> 8 == GIF_HEADER) { + return GIF; + } + + return UNKNOWN; + } + + /** + * Parse the orientation from the image header. If it doesn't handle this image type (or this is not an image) + * it will return a default value rather than throwing an exception. + * + * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't contain an orientation + * @throws IOException + */ + public int getOrientation() throws IOException { + final int magicNumber = streamReader.getUInt16(); + + if (!handles(magicNumber)) { + return -1; + } else { + byte[] exifData = getExifSegment(); + boolean hasJpegExifPreamble = exifData != null + && exifData.length > JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; + + if (hasJpegExifPreamble) { + for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) { + if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) { + hasJpegExifPreamble = false; + break; + } + } + } + + if (hasJpegExifPreamble) { + return parseExifSegment(new RandomAccessReader(exifData)); + } else { + return -1; + } + } + } + + private byte[] getExifSegment() throws IOException { + short segmentId, segmentType; + int segmentLength; + while (true) { + segmentId = streamReader.getUInt8(); + + if (segmentId != SEGMENT_START_ID) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unknown segmentId=" + segmentId); + } + return null; + } + + segmentType = streamReader.getUInt8(); + + if (segmentType == SEGMENT_SOS) { + return null; + } else if (segmentType == MARKER_EOI) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Found MARKER_EOI in exif segment"); + } + return null; + } + + // Segment length includes bytes for segment length. + segmentLength = streamReader.getUInt16() - 2; + + if (segmentType != EXIF_SEGMENT_TYPE) { + long skipped = streamReader.skip(segmentLength); + if (skipped != segmentLength) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unable to skip enough data" + + ", type: " + segmentType + + ", wanted to skip: " + segmentLength + + ", but actually skipped: " + skipped); + } + return null; + } + } else { + byte[] segmentData = new byte[segmentLength]; + int read = streamReader.read(segmentData); + if (read != segmentLength) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unable to read segment data" + + ", type: " + segmentType + + ", length: " + segmentLength + + ", actually read: " + read); + } + return null; + } else { + return segmentData; + } + } + } + } + + private static int parseExifSegment(RandomAccessReader segmentData) { + final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length(); + + short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize); + final ByteOrder byteOrder; + if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) { + byteOrder = ByteOrder.BIG_ENDIAN; + } else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) { + byteOrder = ByteOrder.LITTLE_ENDIAN; + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier); + } + byteOrder = ByteOrder.BIG_ENDIAN; + } + + segmentData.order(byteOrder); + + int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize; + int tagCount = segmentData.getInt16(firstIfdOffset); + + int tagOffset, tagType, formatCode, componentCount; + for (int i = 0; i < tagCount; i++) { + tagOffset = calcTagOffset(firstIfdOffset, i); + + tagType = segmentData.getInt16(tagOffset); + + // We only want orientation. + if (tagType != ORIENTATION_TAG_TYPE) { + continue; + } + + formatCode = segmentData.getInt16(tagOffset + 2); + + // 12 is max format code. + if (formatCode < 1 || formatCode > 12) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got invalid format code=" + formatCode); + } + continue; + } + + componentCount = segmentData.getInt32(tagOffset + 4); + + if (componentCount < 0) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Negative tiff component count"); + } + continue; + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode=" + formatCode + + " componentCount=" + componentCount); + } + + final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode]; + + if (byteCount > 4) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode); + } + continue; + } + + final int tagValueOffset = tagOffset + 8; + + if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType); + } + continue; + } + + if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType); + } + continue; + } + + //assume componentCount == 1 && fmtCode == 3 + return segmentData.getInt16(tagValueOffset); + } + + return -1; + } + + private static int calcTagOffset(int ifdOffset, int tagIndex) { + return ifdOffset + 2 + 12 * tagIndex; + } + + private static boolean handles(int imageMagicNumber) { + return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER + || imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER + || imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER; + } + + private static class RandomAccessReader { + private final ByteBuffer data; + + public RandomAccessReader(byte[] data) { + this.data = ByteBuffer.wrap(data); + this.data.order(ByteOrder.BIG_ENDIAN); + } + + public void order(ByteOrder byteOrder) { + this.data.order(byteOrder); + } + + public int length() { + return data.array().length; + } + + public int getInt32(int offset) { + return data.getInt(offset); + } + + public short getInt16(int offset) { + return data.getShort(offset); + } + } + + private static class StreamReader { + private final InputStream is; + //motorola / big endian byte order + + public StreamReader(InputStream is) { + this.is = is; + } + + public int getUInt16() throws IOException { + return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF); + } + + public short getUInt8() throws IOException { + return (short) (is.read() & 0xFF); + } + + public long skip(long total) throws IOException { + if (total < 0) { + return 0; + } + + long toSkip = total; + while (toSkip > 0) { + long skipped = is.skip(toSkip); + if (skipped > 0) { + toSkip -= skipped; + } else { + // Skip has no specific contract as to what happens when you reach the end of + // the stream. To differentiate between temporarily not having more data and + // having finished the stream, we read a single byte when we fail to skip any + // amount of data. + int testEofByte = is.read(); + if (testEofByte == -1) { + break; + } else { + toSkip--; + } + } + } + return total - toSkip; + } + + public int read(byte[] buffer) throws IOException { + int toRead = buffer.length; + int read; + while (toRead > 0 && ((read = is.read(buffer, buffer.length - toRead, toRead)) != -1)) { + toRead -= read; + } + return buffer.length - toRead; + } + + public int getByte() throws IOException { + return is.read(); + } + } +} + diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoBitmapDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoBitmapDecoder.java new file mode 100755 index 0000000..9527f09 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoBitmapDecoder.java @@ -0,0 +1,61 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; +import android.util.Log; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link ResourceDecoder} that decodes {@link ImageVideoWrapper}s using + * a wrapped {@link ResourceDecoder} for {@link InputStream}s + * and a wrapped {@link ResourceDecoder} for {@link ParcelFileDescriptor}s. + * The {@link InputStream} data in the {@link ImageVideoWrapper} is always preferred. + */ +public class ImageVideoBitmapDecoder implements ResourceDecoder { + private static final String TAG = "ImageVideoDecoder"; + private final ResourceDecoder streamDecoder; + private final ResourceDecoder fileDescriptorDecoder; + + public ImageVideoBitmapDecoder(ResourceDecoder streamDecoder, + ResourceDecoder fileDescriptorDecoder) { + this.streamDecoder = streamDecoder; + this.fileDescriptorDecoder = fileDescriptorDecoder; + } + + @SuppressWarnings("resource") + // @see ResourceDecoder.decode + @Override + public Resource decode(ImageVideoWrapper source, int width, int height) throws IOException { + Resource result = null; + InputStream is = source.getStream(); + if (is != null) { + try { + result = streamDecoder.decode(is, width, height); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Failed to load image from stream, trying FileDescriptor", e); + } + } + } + + if (result == null) { + ParcelFileDescriptor fileDescriptor = source.getFileDescriptor(); + if (fileDescriptor != null) { + result = fileDescriptorDecoder.decode(fileDescriptor, width, height); + } + } + return result; + } + + @Override + public String getId() { + return "ImageVideoBitmapDecoder.com.bumptech.glide.load.resource.bitmap"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoDataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoDataLoadProvider.java new file mode 100755 index 0000000..cabd089 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/ImageVideoDataLoadProvider.java @@ -0,0 +1,56 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.model.ImageVideoWrapperEncoder; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; +import java.io.InputStream; + +/** + * A {@link DataLoadProvider} for loading either an {@link InputStream} or an + * {@link ParcelFileDescriptor} as an {@link Bitmap}. + */ +public class ImageVideoDataLoadProvider implements DataLoadProvider { + private final ImageVideoBitmapDecoder sourceDecoder; + private final ResourceDecoder cacheDecoder; + private final ResourceEncoder encoder; + private final ImageVideoWrapperEncoder sourceEncoder; + + public ImageVideoDataLoadProvider(DataLoadProvider streamBitmapProvider, + DataLoadProvider fileDescriptorBitmapProvider) { + encoder = streamBitmapProvider.getEncoder(); + sourceEncoder = new ImageVideoWrapperEncoder(streamBitmapProvider.getSourceEncoder(), + fileDescriptorBitmapProvider.getSourceEncoder()); + cacheDecoder = streamBitmapProvider.getCacheDecoder(); + sourceDecoder = new ImageVideoBitmapDecoder(streamBitmapProvider.getSourceDecoder(), + fileDescriptorBitmapProvider.getSourceDecoder()); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return sourceDecoder; + } + + @Override + public Encoder getSourceEncoder() { + return sourceEncoder; + } + + @Override + public ResourceEncoder getEncoder() { + return encoder; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStream.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStream.java new file mode 100755 index 0000000..bcbc81b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/RecyclableBufferedInputStream.java @@ -0,0 +1,416 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import android.util.Log; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Wraps an existing {@link InputStream} and buffers the input. + * Expensive interaction with the underlying input stream is minimized, since + * most (smaller) requests can be satisfied by accessing the buffer alone. The + * drawback is that some extra space is required to hold the buffer and that + * copying takes place when filling that buffer, but this is usually outweighed + * by the performance benefits. + * + *

A typical application pattern for the class looks like this:

+ * + *
+ * BufferedInputStream buf = new BufferedInputStream(new FileInputStream("file.java"));
+ * 
+ */ +public class RecyclableBufferedInputStream extends FilterInputStream { + private static final String TAG = "BufferedIs"; + + /** + * The buffer containing the current bytes read from the target InputStream. + */ + private volatile byte[] buf; + + /** + * The total number of bytes inside the byte array {@code buf}. + */ + private int count; + + /** + * The current limit, which when passed, invalidates the current mark. + */ + private int marklimit; + + /** + * The currently marked position. -1 indicates no mark has been set or the + * mark has been invalidated. + */ + private int markpos = -1; + + /** + * The current position within the byte array {@code buf}. + */ + private int pos; + + public RecyclableBufferedInputStream(InputStream in, byte[] buffer) { + super(in); + if (buffer == null || buffer.length == 0) { + throw new IllegalArgumentException("buffer is null or empty"); + } + buf = buffer; + } + + /** + * Returns an estimated number of bytes that can be read or skipped without blocking for more + * input. This method returns the number of bytes available in the buffer + * plus those available in the source stream, but see {@link InputStream#available} for + * important caveats. + * + * @return the estimated number of bytes available + * @throws IOException if this stream is closed or an error occurs + */ + @Override + public synchronized int available() throws IOException { + // in could be invalidated by close(). + InputStream localIn = in; + if (buf == null || localIn == null) { + throw streamClosed(); + } + return count - pos + localIn.available(); + } + + private static IOException streamClosed() throws IOException { + throw new IOException("BufferedInputStream is closed"); + } + + /** + * Reduces the mark limit to match the current buffer length to prevent the buffer from + * continuing to increase in size. + * + *

Subsequent calls to {@link #mark(int)} will be obeyed and may cause the buffer size + * to increase. + */ + public synchronized void fixMarkLimit() { + marklimit = buf.length; + } + + /** + * Closes this stream. The source stream is closed and any resources + * associated with it are released. + * + * @throws IOException + * if an error occurs while closing this stream. + */ + @Override + public void close() throws IOException { + buf = null; + InputStream localIn = in; + in = null; + if (localIn != null) { + localIn.close(); + } + } + + private int fillbuf(InputStream localIn, byte[] localBuf) + throws IOException { + if (markpos == -1 || pos - markpos >= marklimit) { + // Mark position not set or exceeded readlimit + int result = localIn.read(localBuf); + if (result > 0) { + markpos = -1; + pos = 0; + count = result; + } + return result; + } + // Added count == localBuf.length so that we do not immediately double the buffer size before reading any data + // when marklimit > localBuf.length. Instead, we will double the buffer size only after reading the initial + // localBuf worth of data without finding what we're looking for in the stream. This allows us to set a + // relatively small initial buffer size and a large marklimit for safety without causing an allocation each time + // read is called. + if (markpos == 0 && marklimit > localBuf.length && count == localBuf.length) { + // Increase buffer size to accommodate the readlimit + int newLength = localBuf.length * 2; + if (newLength > marklimit) { + newLength = marklimit; + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "allocate buffer of length: " + newLength); + } + byte[] newbuf = new byte[newLength]; + System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length); + // Reassign buf, which will invalidate any local references + // FIXME: what if buf was null? + localBuf = buf = newbuf; + } else if (markpos > 0) { + System.arraycopy(localBuf, markpos, localBuf, 0, localBuf.length + - markpos); + } + // Set the new position and mark position + pos -= markpos; + count = markpos = 0; + int bytesread = localIn.read(localBuf, pos, localBuf.length - pos); + count = bytesread <= 0 ? pos : pos + bytesread; + return bytesread; + } + + /** + * Sets a mark position in this stream. The parameter {@code readlimit} + * indicates how many bytes can be read before a mark is invalidated. + * Calling {@link #reset()} will reposition the stream back to the marked + * position if {@code readlimit} has not been surpassed. The underlying + * buffer may be increased in size to allow {@code readlimit} number of + * bytes to be supported. + * + * @param readlimit + * the number of bytes that can be read before the mark is + * invalidated. + * @see #reset() + */ + @Override + public synchronized void mark(int readlimit) { + // This is stupid, but BitmapFactory.decodeStream calls mark(1024) + // which is too small for a substantial portion of images. This + // change (using Math.max) ensures that we don't overwrite readlimit + // with a smaller value + marklimit = Math.max(marklimit, readlimit); + markpos = pos; + } + + /** + * Indicates whether {@code BufferedInputStream} supports the {@link #mark(int)} + * and {@link #reset()} methods. + * + * @return {@code true} for BufferedInputStreams. + * @see #mark(int) + * @see #reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Reads a single byte from this stream and returns it as an integer in the + * range from 0 to 255. Returns -1 if the end of the source string has been + * reached. If the internal buffer does not contain any available bytes then + * it is filled from the source stream and the first byte is returned. + * + * @return the byte read or -1 if the end of the source stream has been + * reached. + * @throws IOException + * if this stream is closed or another IOException occurs. + */ + @Override + public synchronized int read() throws IOException { + // Use local refs since buf and in may be invalidated by an + // unsynchronized close() + byte[] localBuf = buf; + InputStream localIn = in; + if (localBuf == null || localIn == null) { + throw streamClosed(); + } + + // Are there buffered bytes available? + if (pos >= count && fillbuf(localIn, localBuf) == -1) { + // no, fill buffer + return -1; + } + // localBuf may have been invalidated by fillbuf + if (localBuf != buf) { + localBuf = buf; + if (localBuf == null) { + throw streamClosed(); + } + } + + // Did filling the buffer fail with -1 (EOF)? + if (count - pos > 0) { + return localBuf[pos++] & 0xFF; + } + return -1; + } + + /** + * Reads at most {@code byteCount} bytes from this stream and stores them in + * byte array {@code buffer} starting at offset {@code offset}. Returns the + * number of bytes actually read or -1 if no bytes were read and the end of + * the stream was encountered. If all the buffered bytes have been used, a + * mark has not been set and the requested number of bytes is larger than + * the receiver's buffer size, this implementation bypasses the buffer and + * simply places the results directly into {@code buffer}. + * + * @param buffer + * the byte array in which to store the bytes read. + * @return the number of bytes actually read or -1 if end of stream. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code byteCount < 0}, or if + * {@code offset + byteCount} is greater than the size of + * {@code buffer}. + * @throws IOException + * if the stream is already closed or another IOException + * occurs. + */ + @Override + public synchronized int read(byte[] buffer, int offset, int byteCount) throws IOException { + // Use local ref since buf may be invalidated by an unsynchronized close() + byte[] localBuf = buf; + if (localBuf == null) { + throw streamClosed(); + } + //Arrays.checkOffsetAndCount(buffer.length, offset, byteCount); + if (byteCount == 0) { + return 0; + } + InputStream localIn = in; + if (localIn == null) { + throw streamClosed(); + } + + int required; + if (pos < count) { + // There are bytes available in the buffer. + int copylength = count - pos >= byteCount ? byteCount : count - pos; + System.arraycopy(localBuf, pos, buffer, offset, copylength); + pos += copylength; + if (copylength == byteCount || localIn.available() == 0) { + return copylength; + } + offset += copylength; + required = byteCount - copylength; + } else { + required = byteCount; + } + + while (true) { + int read; + // If we're not marked and the required size is greater than the buffer, + // simply read the bytes directly bypassing the buffer. + if (markpos == -1 && required >= localBuf.length) { + read = localIn.read(buffer, offset, required); + if (read == -1) { + return required == byteCount ? -1 : byteCount - required; + } + } else { + if (fillbuf(localIn, localBuf) == -1) { + return required == byteCount ? -1 : byteCount - required; + } + // localBuf may have been invalidated by fillbuf + if (localBuf != buf) { + localBuf = buf; + if (localBuf == null) { + throw streamClosed(); + } + } + + read = count - pos >= required ? required : count - pos; + System.arraycopy(localBuf, pos, buffer, offset, read); + pos += read; + } + required -= read; + if (required == 0) { + return byteCount; + } + if (localIn.available() == 0) { + return byteCount - required; + } + offset += read; + } + } + + /** + * Resets this stream to the last marked location. + * + * @throws IOException + * if this stream is closed, no mark has been set or the mark is + * no longer valid because more than {@code readlimit} bytes + * have been read since setting the mark. + * @see #mark(int) + */ + @Override + public synchronized void reset() throws IOException { + if (buf == null) { + throw new IOException("Stream is closed"); + } + if (-1 == markpos) { + throw new InvalidMarkException("Mark has been invalidated"); + } + pos = markpos; + } + + /** + * Skips {@code byteCount} bytes in this stream. Subsequent calls to + * {@link #read} will not return these bytes unless {@link #reset} is + * used. + * + * @param byteCount + * the number of bytes to skip. {@link #skip} does nothing and + * returns 0 if {@code byteCount} is less than zero. + * @return the number of bytes actually skipped. + * @throws IOException + * if this stream is closed or another IOException occurs. + */ + @Override + public synchronized long skip(long byteCount) throws IOException { + // Use local refs since buf and in may be invalidated by an unsynchronized close() + byte[] localBuf = buf; + InputStream localIn = in; + if (localBuf == null) { + throw streamClosed(); + } + if (byteCount < 1) { + return 0; + } + if (localIn == null) { + throw streamClosed(); + } + + if (count - pos >= byteCount) { + pos += byteCount; + return byteCount; + } + long read = count - pos; + pos = count; + + if (markpos != -1 && byteCount <= marklimit) { + if (fillbuf(localIn, localBuf) == -1) { + return read; + } + if (count - pos >= byteCount - read) { + pos += byteCount - read; + return byteCount; + } + // Couldn't get all the bytes, skip what we read. + read = read + count - pos; + pos = count; + return read; + } + return read + localIn.skip(byteCount - read); + } + + /** + * An exception thrown when a mark can no longer be obeyed because the underlying buffer size is smaller than the + * amount of data read after the mark position. + */ + public static class InvalidMarkException extends RuntimeException { + private static final long serialVersionUID = -4338378848813561757L; + + public InvalidMarkException(String detailMessage) { + super(detailMessage); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDataLoadProvider.java new file mode 100755 index 0000000..dd1d5b2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDataLoadProvider.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.model.StreamEncoder; +import com.example.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; +import java.io.InputStream; + +/** + * An {@link DataLoadProvider} that provides decoders and encoders for decoding and caching + * {@link Bitmap}s using {@link InputStream} data. + */ +public class StreamBitmapDataLoadProvider implements DataLoadProvider { + private final StreamBitmapDecoder decoder; + private final BitmapEncoder encoder; + private final StreamEncoder sourceEncoder; + private final FileToStreamDecoder cacheDecoder; + + public StreamBitmapDataLoadProvider(BitmapPool bitmapPool, DecodeFormat decodeFormat) { + sourceEncoder = new StreamEncoder(); + decoder = new StreamBitmapDecoder(bitmapPool, decodeFormat); + encoder = new BitmapEncoder(); + cacheDecoder = new FileToStreamDecoder(decoder); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return decoder; + } + + @Override + public Encoder getSourceEncoder() { + return sourceEncoder; + } + + @Override + public ResourceEncoder getEncoder() { + return encoder; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDecoder.java new file mode 100755 index 0000000..348271b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/StreamBitmapDecoder.java @@ -0,0 +1,66 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.content.Context; +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + +import java.io.InputStream; + +/** + * An {@link ResourceDecoder} that uses an + * {@link Downsampler} to decode an {@link Bitmap} from an + * {@link InputStream}. + */ +public class StreamBitmapDecoder implements ResourceDecoder { + private static final String ID = "StreamBitmapDecoder.com.bumptech.glide.load.resource.bitmap"; + private final Downsampler downsampler; + private BitmapPool bitmapPool; + private DecodeFormat decodeFormat; + private String id; + + public StreamBitmapDecoder(Context context) { + this(Glide.get(context).getBitmapPool()); + } + + public StreamBitmapDecoder(BitmapPool bitmapPool) { + this(bitmapPool, DecodeFormat.DEFAULT); + } + + public StreamBitmapDecoder(Context context, DecodeFormat decodeFormat) { + this(Glide.get(context).getBitmapPool(), decodeFormat); + } + + public StreamBitmapDecoder(BitmapPool bitmapPool, DecodeFormat decodeFormat) { + this(Downsampler.AT_LEAST, bitmapPool, decodeFormat); + } + + public StreamBitmapDecoder(Downsampler downsampler, BitmapPool bitmapPool, DecodeFormat decodeFormat) { + this.downsampler = downsampler; + this.bitmapPool = bitmapPool; + this.decodeFormat = decodeFormat; + } + + @Override + public Resource decode(InputStream source, int width, int height) { + Bitmap bitmap = downsampler.decode(source, bitmapPool, width, height, decodeFormat); + return BitmapResource.obtain(bitmap, bitmapPool); + } + + @Override + public String getId() { + if (id == null) { + id = new StringBuilder() + .append(ID) + .append(downsampler.getId()) + .append(decodeFormat.name()) + .toString(); + } + return id; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/TransformationUtils.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/TransformationUtils.java new file mode 100755 index 0000000..d2a88ea --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/TransformationUtils.java @@ -0,0 +1,319 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.os.Build; +import android.util.Log; + +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + +/** + * A class with methods to efficiently resize Bitmaps. + */ +public final class TransformationUtils { + private static final String TAG = "TransformationUtils"; + public static final int PAINT_FLAGS = Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG; + + private TransformationUtils() { + // Utility class. + } + + /** + * A potentially expensive operation to crop the given Bitmap so that it fills the given dimensions. This operation + * is significantly less expensive in terms of memory if a mutable Bitmap with the given dimensions is passed in + * as well. + * + * @param recycled A mutable Bitmap with dimensions width and height that we can load the cropped portion of toCrop + * into. + * @param toCrop The Bitmap to resize. + * @param width The width in pixels of the final Bitmap. + * @param height The height in pixels of the final Bitmap. + * @return The resized Bitmap (will be recycled if recycled is not null). + */ + public static Bitmap centerCrop(Bitmap recycled, Bitmap toCrop, int width, int height) { + if (toCrop == null) { + return null; + } else if (toCrop.getWidth() == width && toCrop.getHeight() == height) { + return toCrop; + } + // From ImageView/Bitmap.createScaledBitmap. + final float scale; + float dx = 0, dy = 0; + Matrix m = new Matrix(); + if (toCrop.getWidth() * height > width * toCrop.getHeight()) { + scale = (float) height / (float) toCrop.getHeight(); + dx = (width - toCrop.getWidth() * scale) * 0.5f; + } else { + scale = (float) width / (float) toCrop.getWidth(); + dy = (height - toCrop.getHeight() * scale) * 0.5f; + } + + m.setScale(scale, scale); + m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); + final Bitmap result; + if (recycled != null) { + result = recycled; + } else { + result = Bitmap.createBitmap(width, height, getSafeConfig(toCrop)); + } + + // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. + TransformationUtils.setAlpha(toCrop, result); + + Canvas canvas = new Canvas(result); + Paint paint = new Paint(PAINT_FLAGS); + canvas.drawBitmap(toCrop, m, paint); + return result; + } + + /** + * An expensive operation to resize the given Bitmap down so that it fits within the given dimensions maintain + * the original proportions. + * + * @param toFit The Bitmap to shrink. + * @param pool The BitmapPool to try to reuse a bitmap from. + * @param width The width in pixels the final image will fit within. + * @param height The height in pixels the final image will fit within. + * @return A new Bitmap shrunk to fit within the given dimensions, or toFit if toFit's width or height matches the + * given dimensions and toFit fits within the given dimensions + */ + public static Bitmap fitCenter(Bitmap toFit, BitmapPool pool, int width, int height) { + if (toFit.getWidth() == width && toFit.getHeight() == height) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "requested target size matches input, returning input"); + } + return toFit; + } + final float widthPercentage = width / (float) toFit.getWidth(); + final float heightPercentage = height / (float) toFit.getHeight(); + final float minPercentage = Math.min(widthPercentage, heightPercentage); + + // take the floor of the target width/height, not round. If the matrix + // passed into drawBitmap rounds differently, we want to slightly + // overdraw, not underdraw, to avoid artifacts from bitmap reuse. + final int targetWidth = (int) (minPercentage * toFit.getWidth()); + final int targetHeight = (int) (minPercentage * toFit.getHeight()); + + if (toFit.getWidth() == targetWidth && toFit.getHeight() == targetHeight) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "adjusted target size matches input, returning input"); + } + return toFit; + } + + Bitmap.Config config = getSafeConfig(toFit); + Bitmap toReuse = pool.get(targetWidth, targetHeight, config); + if (toReuse == null) { + toReuse = Bitmap.createBitmap(targetWidth, targetHeight, config); + } + // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. + TransformationUtils.setAlpha(toFit, toReuse); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "request: " + width + "x" + height); + Log.v(TAG, "toFit: " + toFit.getWidth() + "x" + toFit.getHeight()); + Log.v(TAG, "toReuse: " + toReuse.getWidth() + "x" + toReuse.getHeight()); + Log.v(TAG, "minPct: " + minPercentage); + } + + Canvas canvas = new Canvas(toReuse); + Matrix matrix = new Matrix(); + matrix.setScale(minPercentage, minPercentage); + Paint paint = new Paint(PAINT_FLAGS); + canvas.drawBitmap(toFit, matrix, paint); + + return toReuse; + } + + /** + * Sets the alpha of the Bitmap we're going to re-use to the alpha of the Bitmap we're going to transform. This + * keeps {@link Bitmap#hasAlpha()}} consistent before and after the transformation for + * transformations that don't add or remove transparent pixels. + * + * @param toTransform The {@link Bitmap} that will be transformed. + * @param outBitmap The {@link Bitmap} that will be returned from the transformation. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) + public static void setAlpha(Bitmap toTransform, Bitmap outBitmap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1 && outBitmap != null) { + outBitmap.setHasAlpha(toTransform.hasAlpha()); + } + } + + /** + * Returns a matrix with rotation set based on Exif orientation tag. + * If the orientation is undefined or 0 null is returned. + * + * @deprecated No longer used by Glide, scheduled to be removed in Glide 4.0 + * @param pathToOriginal Path to original image file that may have exif data. + * @return A rotation in degrees based on exif orientation + */ + @TargetApi(Build.VERSION_CODES.ECLAIR) + @Deprecated + public static int getOrientation(String pathToOriginal) { + int degreesToRotate = 0; + try { + ExifInterface exif = new ExifInterface(pathToOriginal); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + return getExifOrientationDegrees(orientation); + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "Unable to get orientation for image with path=" + pathToOriginal, e); + } + } + return degreesToRotate; + } + + /** + * This is an expensive operation that copies the image in place with the pixels rotated. + * If possible rather use getOrientationMatrix, and set that as the imageMatrix on an ImageView. + * + * @deprecated No longer used by Glide, scheduled to be removed in Glide 4.0 + * @param pathToOriginal Path to original image file that may have exif data. + * @param imageToOrient Image Bitmap to orient. + * @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap. + */ + @Deprecated + public static Bitmap orientImage(String pathToOriginal, Bitmap imageToOrient) { + int degreesToRotate = getOrientation(pathToOriginal); + return rotateImage(imageToOrient, degreesToRotate); + } + + /** + * This is an expensive operation that copies the image in place with the pixels rotated. + * If possible rather use getOrientationMatrix, and set that as the imageMatrix on an ImageView. + * + * @param imageToOrient Image Bitmap to orient. + * @param degreesToRotate number of degrees to rotate the image by. If zero the original image is returned + * unmodified. + * @return The oriented bitmap. May be the imageToOrient without modification, or a new Bitmap. + */ + public static Bitmap rotateImage(Bitmap imageToOrient, int degreesToRotate) { + Bitmap result = imageToOrient; + try { + if (degreesToRotate != 0) { + Matrix matrix = new Matrix(); + matrix.setRotate(degreesToRotate); + result = Bitmap.createBitmap( + imageToOrient, + 0, + 0, + imageToOrient.getWidth(), + imageToOrient.getHeight(), + matrix, + true); + } + } catch (Exception e) { + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "Exception when trying to orient image", e); + } + } + return result; + } + + /** + * Get the # of degrees an image must be rotated to match the given exif orientation. + * + * @param exifOrientation The exif orientation [1-8] + * @return the number of degrees to rotate + */ + public static int getExifOrientationDegrees(int exifOrientation) { + final int degreesToRotate; + switch (exifOrientation) { + case ExifInterface.ORIENTATION_TRANSPOSE: + case ExifInterface.ORIENTATION_ROTATE_90: + degreesToRotate = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + degreesToRotate = 180; + break; + case ExifInterface.ORIENTATION_TRANSVERSE: + case ExifInterface.ORIENTATION_ROTATE_270: + degreesToRotate = 270; + break; + default: + degreesToRotate = 0; + + } + return degreesToRotate; + } + + /** + * Rotate and/or flip the image to match the given exif orientation. + * + * @param toOrient The bitmap to rotate/flip. + * @param pool A pool that may or may not contain an image of the necessary dimensions. + * @param exifOrientation the exif orientation [1-8]. + * @return The rotated and/or flipped image or toOrient if no rotation or flip was necessary. + */ + public static Bitmap rotateImageExif(Bitmap toOrient, BitmapPool pool, int exifOrientation) { + final Matrix matrix = new Matrix(); + initializeMatrixForRotation(exifOrientation, matrix); + if (matrix.isIdentity()) { + return toOrient; + } + + // From Bitmap.createBitmap. + final RectF newRect = new RectF(0, 0, toOrient.getWidth(), toOrient.getHeight()); + matrix.mapRect(newRect); + + final int newWidth = Math.round(newRect.width()); + final int newHeight = Math.round(newRect.height()); + + Bitmap.Config config = getSafeConfig(toOrient); + Bitmap result = pool.get(newWidth, newHeight, config); + if (result == null) { + result = Bitmap.createBitmap(newWidth, newHeight, config); + } + + matrix.postTranslate(-newRect.left, -newRect.top); + + final Canvas canvas = new Canvas(result); + final Paint paint = new Paint(PAINT_FLAGS); + canvas.drawBitmap(toOrient, matrix, paint); + + return result; + } + + private static Bitmap.Config getSafeConfig(Bitmap bitmap) { + return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; + } + + // Visible for testing. + static void initializeMatrixForRotation(int exifOrientation, Matrix matrix) { + switch (exifOrientation) { + case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: + matrix.setScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_180: + matrix.setRotate(180); + break; + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + matrix.setRotate(180); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_TRANSPOSE: + matrix.setRotate(90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_90: + matrix.setRotate(90); + break; + case ExifInterface.ORIENTATION_TRANSVERSE: + matrix.setRotate(-90); + matrix.postScale(-1, 1); + break; + case ExifInterface.ORIENTATION_ROTATE_270: + matrix.setRotate(-90); + break; + default: + // Do nothing. + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java new file mode 100755 index 0000000..b694dc0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java @@ -0,0 +1,77 @@ +package com.example.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.os.ParcelFileDescriptor; + + +import com.example.bumptech.glide.load.DecodeFormat; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + +import java.io.IOException; + +/** + * An {@link BitmapDecoder} that can decode a thumbnail frame + * {@link Bitmap} from a {@link ParcelFileDescriptor} containing a video. + * + * @see MediaMetadataRetriever + */ +public class VideoBitmapDecoder implements BitmapDecoder { + private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY = new MediaMetadataRetrieverFactory(); + private static final int NO_FRAME = -1; + private MediaMetadataRetrieverFactory factory; + private int frame; + + public VideoBitmapDecoder() { + this(DEFAULT_FACTORY, NO_FRAME); + } + + public VideoBitmapDecoder(int frame) { + this(DEFAULT_FACTORY, checkValidFrame(frame)); + } + + VideoBitmapDecoder(MediaMetadataRetrieverFactory factory) { + this(factory, NO_FRAME); + } + + VideoBitmapDecoder(MediaMetadataRetrieverFactory factory, int frame) { + this.factory = factory; + this.frame = frame; + } + + @Override + public Bitmap decode(ParcelFileDescriptor resource, BitmapPool bitmapPool, int outWidth, int outHeight, + DecodeFormat decodeFormat) + throws IOException { + MediaMetadataRetriever mediaMetadataRetriever = factory.build(); + mediaMetadataRetriever.setDataSource(resource.getFileDescriptor()); + Bitmap result; + if (frame >= 0) { + result = mediaMetadataRetriever.getFrameAtTime(frame); + } else { + result = mediaMetadataRetriever.getFrameAtTime(); + } + mediaMetadataRetriever.release(); + resource.close(); + return result; + } + + @Override + public String getId() { + return "VideoBitmapDecoder.com.bumptech.glide.load.resource.bitmap"; + } + + // Visible for testing. + static class MediaMetadataRetrieverFactory { + public MediaMetadataRetriever build() { + return new MediaMetadataRetriever(); + } + } + + private static int checkValidFrame(int frame) { + if (frame < 0) { + throw new IllegalArgumentException("Requested frame must be non-negative"); + } + return frame; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/bytes/BytesResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/bytes/BytesResource.java new file mode 100755 index 0000000..942c321 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/bytes/BytesResource.java @@ -0,0 +1,33 @@ +package com.example.bumptech.glide.load.resource.bytes; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * An {@link Resource} wrapping a byte array. + */ +public class BytesResource implements Resource { + private final byte[] bytes; + + public BytesResource(byte[] bytes) { + if (bytes == null) { + throw new NullPointerException("Bytes must not be null"); + } + this.bytes = bytes; + } + + @Override + public byte[] get() { + return bytes; + } + + @Override + public int getSize() { + return bytes.length; + } + + @Override + public void recycle() { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/DrawableResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/DrawableResource.java new file mode 100755 index 0000000..9d0768e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/DrawableResource.java @@ -0,0 +1,35 @@ +package com.example.bumptech.glide.load.resource.drawable; + +import android.graphics.drawable.Drawable; + +import com.example.bumptech.glide.load.engine.Resource; + + +/** + * Simple wrapper for an Android {@link Drawable} which returns a + * {@link Drawable.ConstantState#newDrawable() new drawable} + * based on it's {@link Drawable.ConstantState state}. + * + * Suggested usages only include {@code T}s where the new drawable is of the same or descendant class. + * + * @param type of the wrapped {@link Drawable} + */ +public abstract class DrawableResource implements Resource { + protected final T drawable; + + public DrawableResource(T drawable) { + if (drawable == null) { + throw new NullPointerException("Drawable must not be null!"); + } + this.drawable = drawable; + } + + @SuppressWarnings("unchecked") + @Override + public final T get() { + // Drawables contain temporary state related to how they're being displayed (alpha, color filter etc), so + // return a new copy each time. If we ever return the original drawable, it's temporary state may be changed + // and subsequent copies may end up with that temporary state. See #276. + return (T) drawable.getConstantState().newDrawable(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/GlideDrawable.java b/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/GlideDrawable.java new file mode 100755 index 0000000..bcbcbfb --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/drawable/GlideDrawable.java @@ -0,0 +1,29 @@ +package com.example.bumptech.glide.load.resource.drawable; + +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; + +/** + * A base class for drawables that are either static equivalents of {@link android.graphics.drawable.BitmapDrawable} or + * that contain an animation. + */ +public abstract class GlideDrawable extends Drawable implements Animatable { + /** A constant indicating that an animated drawable should loop continuously. */ + public static final int LOOP_FOREVER = -1; + /** + * A constant indicating that an animated drawable should loop for its default number of times. For animated GIFs, + * this constant indicates the GIF should use the netscape loop count if present. + */ + public static final int LOOP_INTRINSIC = 0; + + /** + * Returns {@code true} if this drawable is animated. + */ + public abstract boolean isAnimated(); + + /** + * Sets the number of times the animation should loop. This method will only have an affect if + * {@link #isAnimated ()}} returns {@code true}. A loop count of <=0 indicates loop forever. + */ + public abstract void setLoopCount(int loopCount); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileDecoder.java new file mode 100755 index 0000000..4e9c6a1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileDecoder.java @@ -0,0 +1,23 @@ +package com.example.bumptech.glide.load.resource.file; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; + +import java.io.File; + +/** + * A simple {@link ResourceDecoder} that creates resource for a given {@link File}. + */ +public class FileDecoder implements ResourceDecoder { + + @Override + public Resource decode(File source, int width, int height) { + return new FileResource(source); + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileResource.java new file mode 100755 index 0000000..3ee5704 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileResource.java @@ -0,0 +1,15 @@ +package com.example.bumptech.glide.load.resource.file; + + +import com.example.bumptech.glide.load.resource.SimpleResource; + +import java.io.File; + +/** + * A simple {@link com.bumptech.glide.load.engine.Resource} that wraps a {@link File}. + */ +public class FileResource extends SimpleResource { + public FileResource(File file) { + super(file); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileToStreamDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileToStreamDecoder.java new file mode 100755 index 0000000..47cf7d3 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/file/FileToStreamDecoder.java @@ -0,0 +1,64 @@ +package com.example.bumptech.glide.load.resource.file; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A decoder that wraps an {@link InputStream} decoder to allow it to decode from a file. + * + * @param The type of resource that the wrapped InputStream decoder decodes. + */ +public class FileToStreamDecoder implements ResourceDecoder { + private static final FileOpener DEFAULT_FILE_OPENER = new FileOpener(); + + private ResourceDecoder streamDecoder; + private final FileOpener fileOpener; + + public FileToStreamDecoder(ResourceDecoder streamDecoder) { + this(streamDecoder, DEFAULT_FILE_OPENER); + } + + // Exposed for testing. + FileToStreamDecoder(ResourceDecoder streamDecoder, FileOpener fileOpener) { + this.streamDecoder = streamDecoder; + this.fileOpener = fileOpener; + } + + @Override + public Resource decode(File source, int width, int height) throws IOException { + InputStream is = null; + Resource result = null; + try { + is = fileOpener.open(source); + result = streamDecoder.decode(is, width, height); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Do nothing. + } + } + } + return result; + } + + @Override + public String getId() { + return ""; + } + + // Visible for testing. + static class FileOpener { + public InputStream open(File file) throws FileNotFoundException { + return new FileInputStream(file); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/file/StreamFileDataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/file/StreamFileDataLoadProvider.java new file mode 100755 index 0000000..e95486d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/file/StreamFileDataLoadProvider.java @@ -0,0 +1,63 @@ +package com.example.bumptech.glide.load.resource.file; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.model.StreamEncoder; +import com.example.bumptech.glide.load.resource.NullResourceEncoder; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; +import java.io.InputStream; + +/** + * An {@link DataLoadProvider} that provides encoders and decoders for for obtaining a + * cache file from {@link InputStream} data. + */ +public class StreamFileDataLoadProvider implements DataLoadProvider { + private static final ErrorSourceDecoder ERROR_DECODER = new ErrorSourceDecoder(); + + private final ResourceDecoder cacheDecoder; + private final Encoder encoder; + + public StreamFileDataLoadProvider() { + cacheDecoder = new FileDecoder(); + encoder = new StreamEncoder(); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return ERROR_DECODER; + } + + @Override + public Encoder getSourceEncoder() { + return encoder; + } + + @Override + public ResourceEncoder getEncoder() { + return NullResourceEncoder.get(); + } + + private static class ErrorSourceDecoder implements ResourceDecoder { + @Override + public Resource decode(InputStream source, int width, int height) { + throw new Error("You cannot decode a File from an InputStream by default," + + " try either #diskCacheStratey(DiskCacheStrategy.SOURCE) to avoid this call or" + + " #decoder(ResourceDecoder) to replace this Decoder"); + } + + @Override + public String getId() { + return ""; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifBitmapProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifBitmapProvider.java new file mode 100755 index 0000000..1825337 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifBitmapProvider.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.load.resource.gif; + + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; + + +class GifBitmapProvider implements GifDecoder.BitmapProvider { + private final BitmapPool bitmapPool; + + public GifBitmapProvider(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + } + + @Override + public Bitmap obtain(int width, int height, Bitmap.Config config) { + return bitmapPool.getDirty(width, height, config); + } + + @Override + public void release(Bitmap bitmap) { + if (!bitmapPool.put(bitmap)) { + bitmap.recycle(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawable.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawable.java new file mode 100755 index 0000000..bea2f12 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawable.java @@ -0,0 +1,377 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Gravity; + +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.gifdecoder.GifHeader; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; + + +/** + * An animated {@link Drawable} that plays the frames of an animated GIF. + */ +public class GifDrawable extends GlideDrawable implements GifFrameLoader.FrameCallback { + private final Paint paint; + private final Rect destRect = new Rect(); + private final GifState state; + private final GifDecoder decoder; + private final GifFrameLoader frameLoader; + + /** True if the drawable is currently animating. */ + private boolean isRunning; + /** True if the drawable should animate while visible. */ + private boolean isStarted; + /** True if the drawable's resources have been recycled. */ + private boolean isRecycled; + /** + * True if the drawable is currently visible. Default to true because on certain platforms (at least 4.1.1), + * setVisible is not called on {@link Drawable Drawables} during + * {@link android.widget.ImageView#setImageDrawable(Drawable)}. See issue #130. + */ + private boolean isVisible = true; + /** The number of times we've looped over all the frames in the gif. */ + private int loopCount; + /** The number of times to loop through the gif animation. */ + private int maxLoopCount = LOOP_FOREVER; + + private boolean applyGravity; + + /** + * Constructor for GifDrawable. + * + * @see #setFrameTransformation(Transformation, Bitmap) + * + * @param context A context. + * @param bitmapProvider An {@link GifDecoder.BitmapProvider} that can be used to + * retrieve re-usable {@link Bitmap}s. + * @param bitmapPool A {@link BitmapPool} that can be used to return + * the first frame when this drawable is recycled. + * @param frameTransformation An {@link Transformation} that can be applied to each frame. + * @param targetFrameWidth The desired width of the frames displayed by this drawable (the width of the view or + * {@link com.bumptech.glide.request.target.Target} this drawable is being loaded into). + * @param targetFrameHeight The desired height of the frames displayed by this drawable (the height of the view or + * {@link com.bumptech.glide.request.target.Target} this drawable is being loaded into). + * @param gifHeader The header data for this gif. + * @param data The full bytes of the gif. + * @param firstFrame The decoded and transformed first frame of this gif. + */ + public GifDrawable(Context context, GifDecoder.BitmapProvider bitmapProvider, BitmapPool bitmapPool, + Transformation frameTransformation, int targetFrameWidth, int targetFrameHeight, + GifHeader gifHeader, byte[] data, Bitmap firstFrame) { + this(new GifState(gifHeader, data, context, frameTransformation, targetFrameWidth, targetFrameHeight, + bitmapProvider, bitmapPool, firstFrame)); + } + + public GifDrawable(GifDrawable other, Bitmap firstFrame, + Transformation frameTransformation) { + this(new GifState(other.state.gifHeader, other.state.data, other.state.context, + frameTransformation, other.state.targetWidth, other.state.targetHeight, + other.state.bitmapProvider, other.state.bitmapPool, firstFrame)); + } + + GifDrawable(GifState state) { + if (state == null) { + throw new NullPointerException("GifState must not be null"); + } + + this.state = state; + this.decoder = new GifDecoder(state.bitmapProvider); + this.paint = new Paint(); + decoder.setData(state.gifHeader, state.data); + frameLoader = new GifFrameLoader(state.context, this, decoder, state.targetWidth, state.targetHeight); + frameLoader.setFrameTransformation(state.frameTransformation); + } + + // Visible for testing. + GifDrawable(GifDecoder decoder, GifFrameLoader frameLoader, Bitmap firstFrame, BitmapPool bitmapPool, Paint paint) { + this.decoder = decoder; + this.frameLoader = frameLoader; + this.state = new GifState(null); + this.paint = paint; + state.bitmapPool = bitmapPool; + state.firstFrame = firstFrame; + } + + public Bitmap getFirstFrame() { + return state.firstFrame; + } + + public void setFrameTransformation(Transformation frameTransformation, Bitmap firstFrame) { + if (firstFrame == null) { + throw new NullPointerException("The first frame of the GIF must not be null"); + } + if (frameTransformation == null) { + throw new NullPointerException("The frame transformation must not be null"); + } + state.frameTransformation = frameTransformation; + state.firstFrame = firstFrame; + frameLoader.setFrameTransformation(frameTransformation); + } + + public GifDecoder getDecoder() { + return decoder; + } + + public Transformation getFrameTransformation() { + return state.frameTransformation; + } + + public byte[] getData() { + return state.data; + } + + public int getFrameCount() { + return decoder.getFrameCount(); + } + + private void resetLoopCount() { + loopCount = 0; + } + + @Override + public void start() { + isStarted = true; + resetLoopCount(); + if (isVisible) { + startRunning(); + } + } + + @Override + public void stop() { + isStarted = false; + stopRunning(); + + // On APIs > honeycomb we know our drawable is not being displayed anymore when it's callback is cleared and so + // we can use the absence of a callback as an indication that it's ok to clear our temporary data. Prior to + // honeycomb we can't tell if our callback is null and instead eagerly reset to avoid holding on to resources we + // no longer need. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + reset(); + } + } + + /** + * Clears temporary data and resets the drawable back to the first frame. + */ + private void reset() { + frameLoader.clear(); + invalidateSelf(); + } + + private void startRunning() { + // If we have only a single frame, we don't want to decode it endlessly. + if (decoder.getFrameCount() == 1) { + invalidateSelf(); + } else if (!isRunning) { + isRunning = true; + frameLoader.start(); + invalidateSelf(); + } + } + + private void stopRunning() { + isRunning = false; + frameLoader.stop(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + isVisible = visible; + if (!visible) { + stopRunning(); + } else if (isStarted) { + startRunning(); + } + return super.setVisible(visible, restart); + } + + @Override + public int getIntrinsicWidth() { + return state.firstFrame.getWidth(); + } + + @Override + public int getIntrinsicHeight() { + return state.firstFrame.getHeight(); + } + + @Override + public boolean isRunning() { + return isRunning; + } + + // For testing. + void setIsRunning(boolean isRunning) { + this.isRunning = isRunning; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + applyGravity = true; + } + + @Override + public void draw(Canvas canvas) { + if (isRecycled) { + return; + } + + if (applyGravity) { + Gravity.apply(GifState.GRAVITY, getIntrinsicWidth(), getIntrinsicHeight(), getBounds(), destRect); + applyGravity = false; + } + + Bitmap currentFrame = frameLoader.getCurrentFrame(); + Bitmap toDraw = currentFrame != null ? currentFrame : state.firstFrame; + canvas.drawBitmap(toDraw, null, destRect, paint); + } + + @Override + public void setAlpha(int i) { + paint.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + // We can't tell, so default to transparent to be safe. + return PixelFormat.TRANSPARENT; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public void onFrameReady(int frameIndex) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && getCallback() == null) { + stop(); + reset(); + return; + } + + invalidateSelf(); + + if (frameIndex == decoder.getFrameCount() - 1) { + loopCount++; + } + + if (maxLoopCount != LOOP_FOREVER && loopCount >= maxLoopCount) { + stop(); + } + } + + @Override + public Drawable.ConstantState getConstantState() { + return state; + } + + /** + * Clears any resources for loading frames that are currently held on to by this object. + */ + public void recycle() { + isRecycled = true; + state.bitmapPool.put(state.firstFrame); + frameLoader.clear(); + frameLoader.stop(); + } + + // For testing. + boolean isRecycled() { + return isRecycled; + } + + @Override + public boolean isAnimated() { + return true; + } + + @Override + public void setLoopCount(int loopCount) { + if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) { + throw new IllegalArgumentException("Loop count must be greater than 0, or equal to " + + "GlideDrawable.LOOP_FOREVER, or equal to GlideDrawable.LOOP_INTRINSIC"); + } + + if (loopCount == LOOP_INTRINSIC) { + maxLoopCount = decoder.getLoopCount(); + } else { + maxLoopCount = loopCount; + } + } + + static class GifState extends Drawable.ConstantState { + private static final int GRAVITY = Gravity.FILL; + GifHeader gifHeader; + byte[] data; + Context context; + Transformation frameTransformation; + int targetWidth; + int targetHeight; + GifDecoder.BitmapProvider bitmapProvider; + BitmapPool bitmapPool; + Bitmap firstFrame; + + public GifState(GifHeader header, byte[] data, Context context, + Transformation frameTransformation, int targetWidth, int targetHeight, + GifDecoder.BitmapProvider provider, BitmapPool bitmapPool, Bitmap firstFrame) { + if (firstFrame == null) { + throw new NullPointerException("The first frame of the GIF must not be null"); + } + gifHeader = header; + this.data = data; + this.bitmapPool = bitmapPool; + this.firstFrame = firstFrame; + this.context = context.getApplicationContext(); + this.frameTransformation = frameTransformation; + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + bitmapProvider = provider; + } + + public GifState(GifState original) { + if (original != null) { + gifHeader = original.gifHeader; + data = original.data; + context = original.context; + frameTransformation = original.frameTransformation; + targetWidth = original.targetWidth; + targetHeight = original.targetHeight; + bitmapProvider = original.bitmapProvider; + bitmapPool = original.bitmapPool; + firstFrame = original.firstFrame; + } + } + + @Override + public Drawable newDrawable(Resources res) { + return newDrawable(); + } + + @Override + public Drawable newDrawable() { + return new GifDrawable(this); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableLoadProvider.java new file mode 100755 index 0000000..105fe51 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableLoadProvider.java @@ -0,0 +1,53 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.content.Context; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.model.StreamEncoder; +import com.example.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; +import java.io.InputStream; + +/** + * An {@link DataLoadProvider} that loads an {@link InputStream} into + * {@link GifDrawable} that can be used to display an animated GIF. + */ +public class GifDrawableLoadProvider implements DataLoadProvider { + private final GifResourceDecoder decoder; + private final GifResourceEncoder encoder; + private final StreamEncoder sourceEncoder; + private final FileToStreamDecoder cacheDecoder; + + public GifDrawableLoadProvider(Context context, BitmapPool bitmapPool) { + decoder = new GifResourceDecoder(context, bitmapPool); + cacheDecoder = new FileToStreamDecoder(decoder); + encoder = new GifResourceEncoder(bitmapPool); + sourceEncoder = new StreamEncoder(); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return decoder; + } + + @Override + public Encoder getSourceEncoder() { + return sourceEncoder; + } + + @Override + public ResourceEncoder getEncoder() { + return encoder; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableResource.java new file mode 100755 index 0000000..4e43dfd --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableResource.java @@ -0,0 +1,25 @@ +package com.example.bumptech.glide.load.resource.gif; + + +import com.example.bumptech.glide.load.resource.drawable.DrawableResource; +import com.example.bumptech.glide.util.Util; + +/** + * A resource wrapping an {@link GifDrawable}. + */ +public class GifDrawableResource extends DrawableResource { + public GifDrawableResource(GifDrawable drawable) { + super(drawable); + } + + @Override + public int getSize() { + return drawable.getData().length + Util.getBitmapByteSize(drawable.getFirstFrame()); + } + + @Override + public void recycle() { + drawable.stop(); + drawable.recycle(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableTransformation.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableTransformation.java new file mode 100755 index 0000000..3a747f0 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifDrawableTransformation.java @@ -0,0 +1,47 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; + + +/** + * An {@link Transformation} that wraps a transformation for a {@link Bitmap} + * and can apply it to every frame of any {@link GifDrawable}. + */ +public class GifDrawableTransformation implements Transformation { + private final Transformation wrapped; + private final BitmapPool bitmapPool; + + public GifDrawableTransformation(Transformation wrapped, BitmapPool bitmapPool) { + this.wrapped = wrapped; + this.bitmapPool = bitmapPool; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + GifDrawable drawable = resource.get(); + + // The drawable needs to be initialized with the correct width and height in order for a view displaying it + // to end up with the right dimensions. Since our transformations may arbitrarily modify the dimensions of + // our gif, here we create a stand in for a frame and pass it to the transformation to see what the final + // transformed dimensions will be so that our drawable can report the correct intrinsic width and height. + Bitmap firstFrame = resource.get().getFirstFrame(); + Resource bitmapResource = new BitmapResource(firstFrame, bitmapPool); + Resource transformed = wrapped.transform(bitmapResource, outWidth, outHeight); + Bitmap transformedFrame = transformed.get(); + if (!transformedFrame.equals(firstFrame)) { + return new GifDrawableResource(new GifDrawable(drawable, transformedFrame, wrapped)); + } else { + return resource; + } + } + + @Override + public String getId() { + return wrapped.getId(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameLoader.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameLoader.java new file mode 100755 index 0000000..296584d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameLoader.java @@ -0,0 +1,220 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; + + +import com.example.bumptech.glide.GenericRequestBuilder; +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.NullEncoder; +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.target.SimpleTarget; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.UUID; + +class GifFrameLoader { + + private final FrameCallback callback; + private final GifDecoder gifDecoder; + private final Handler handler; + + private boolean isRunning = false; + private boolean isLoadPending = false; + private GenericRequestBuilder requestBuilder; + private DelayTarget current; + private boolean isCleared; + + public interface FrameCallback { + void onFrameReady(int index); + } + + public GifFrameLoader(Context context, FrameCallback callback, GifDecoder gifDecoder, int width, int height) { + this(callback, gifDecoder, null, + getRequestBuilder(context, gifDecoder, width, height, Glide.get(context).getBitmapPool())); + } + + GifFrameLoader(FrameCallback callback, GifDecoder gifDecoder, Handler handler, + GenericRequestBuilder requestBuilder) { + if (handler == null) { + handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback()); + } + this.callback = callback; + this.gifDecoder = gifDecoder; + this.handler = handler; + this.requestBuilder = requestBuilder; + } + + @SuppressWarnings("unchecked") + public void setFrameTransformation(Transformation transformation) { + if (transformation == null) { + throw new NullPointerException("Transformation must not be null"); + } + requestBuilder = requestBuilder.transform(transformation); + } + + public void start() { + if (isRunning) { + return; + } + isRunning = true; + isCleared = false; + + loadNextFrame(); + } + + public void stop() { + isRunning = false; + } + + public void clear() { + stop(); + if (current != null) { + Glide.clear(current); + current = null; + } + isCleared = true; + // test. + } + + public Bitmap getCurrentFrame() { + return current != null ? current.getResource() : null; + } + + private void loadNextFrame() { + if (!isRunning || isLoadPending) { + return; + } + isLoadPending = true; + + gifDecoder.advance(); + long targetTime = SystemClock.uptimeMillis() + gifDecoder.getNextDelay(); + DelayTarget next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime); + requestBuilder + .signature(new FrameSignature()) + .into(next); + } + + // Visible for testing. + void onFrameReady(DelayTarget delayTarget) { + if (isCleared) { + handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, delayTarget).sendToTarget(); + return; + } + + DelayTarget previous = current; + current = delayTarget; + callback.onFrameReady(delayTarget.index); + + if (previous != null) { + handler.obtainMessage(FrameLoaderCallback.MSG_CLEAR, previous).sendToTarget(); + } + + isLoadPending = false; + loadNextFrame(); + } + + private class FrameLoaderCallback implements Handler.Callback { + public static final int MSG_DELAY = 1; + public static final int MSG_CLEAR = 2; + + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MSG_DELAY) { + DelayTarget target = (DelayTarget) msg.obj; + onFrameReady(target); + return true; + } else if (msg.what == MSG_CLEAR) { + DelayTarget target = (DelayTarget) msg.obj; + Glide.clear(target); + } + return false; + } + } + + // Visible for testing. + static class DelayTarget extends SimpleTarget { + private final Handler handler; + private final int index; + private final long targetTime; + private Bitmap resource; + + public DelayTarget(Handler handler, int index, long targetTime) { + this.handler = handler; + this.index = index; + this.targetTime = targetTime; + } + + public Bitmap getResource() { + return resource; + } + + @Override + public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { + this.resource = resource; + Message msg = handler.obtainMessage(FrameLoaderCallback.MSG_DELAY, this); + handler.sendMessageAtTime(msg, targetTime); + } + } + + private static GenericRequestBuilder getRequestBuilder(Context context, + GifDecoder gifDecoder, int width, int height, BitmapPool bitmapPool) { + GifFrameResourceDecoder frameResourceDecoder = new GifFrameResourceDecoder(bitmapPool); + GifFrameModelLoader frameLoader = new GifFrameModelLoader(); + Encoder sourceEncoder = NullEncoder.get(); + return Glide.with(context) + .using(frameLoader, GifDecoder.class) + .load(gifDecoder) + .as(Bitmap.class) + .sourceEncoder(sourceEncoder) + .decoder(frameResourceDecoder) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .override(width, height); + + } + + // Visible for testing. + static class FrameSignature implements Key { + private final UUID uuid; + + public FrameSignature() { + this(UUID.randomUUID()); + } + + // VisibleForTesting. + FrameSignature(UUID uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(Object o) { + if (o instanceof FrameSignature) { + FrameSignature other = (FrameSignature) o; + return other.uuid.equals(uuid); + } + return false; + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + throw new UnsupportedOperationException("Not implemented"); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameModelLoader.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameModelLoader.java new file mode 100755 index 0000000..fbe9eed --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameModelLoader.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.resource.gif; + + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.model.ModelLoader; + +class GifFrameModelLoader implements ModelLoader { + + @Override + public DataFetcher getResourceFetcher(GifDecoder model, int width, int height) { + return new GifFrameDataFetcher(model); + } + + private static class GifFrameDataFetcher implements DataFetcher { + private final GifDecoder decoder; + + public GifFrameDataFetcher(GifDecoder decoder) { + this.decoder = decoder; + } + + @Override + public GifDecoder loadData(Priority priority) { + return decoder; + } + + @Override + public void cleanup() { + // Do nothing. GifDecoder reads from an arbitrary InputStream, the caller will close that stream. + } + + @Override + public String getId() { + return String.valueOf(decoder.getCurrentFrameIndex()); + } + + @Override + public void cancel() { + // Do nothing. + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java new file mode 100755 index 0000000..ba362b8 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifFrameResourceDecoder.java @@ -0,0 +1,29 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; + + +class GifFrameResourceDecoder implements ResourceDecoder { + private final BitmapPool bitmapPool; + + public GifFrameResourceDecoder(BitmapPool bitmapPool) { + this.bitmapPool = bitmapPool; + } + + @Override + public Resource decode(GifDecoder source, int width, int height) { + Bitmap bitmap = source.getNextFrame(); + return BitmapResource.obtain(bitmap, bitmapPool); + } + + @Override + public String getId() { + return "GifFrameResourceDecoder.com.bumptech.glide.load.resource.gif"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceDecoder.java new file mode 100755 index 0000000..64463da --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceDecoder.java @@ -0,0 +1,152 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.gifdecoder.GifHeader; +import com.example.bumptech.glide.gifdecoder.GifHeaderParser; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.UnitTransformation; +import com.example.bumptech.glide.util.Util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Queue; + +/** + * An {@link ResourceDecoder} that decodes + * {@link GifDrawable} from {@link InputStream} data. + */ +public class GifResourceDecoder implements ResourceDecoder { + private static final String TAG = "GifResourceDecoder"; + private static final GifHeaderParserPool PARSER_POOL = new GifHeaderParserPool(); + private static final GifDecoderPool DECODER_POOL = new GifDecoderPool(); + + private final Context context; + private final GifHeaderParserPool parserPool; + private final BitmapPool bitmapPool; + private final GifDecoderPool decoderPool; + private final GifBitmapProvider provider; + + public GifResourceDecoder(Context context) { + this(context, Glide.get(context).getBitmapPool()); + } + + public GifResourceDecoder(Context context, BitmapPool bitmapPool) { + this(context, bitmapPool, PARSER_POOL, DECODER_POOL); + } + + // Visible for testing. + GifResourceDecoder(Context context, BitmapPool bitmapPool, GifHeaderParserPool parserPool, + GifDecoderPool decoderPool) { + this.context = context; + this.bitmapPool = bitmapPool; + this.decoderPool = decoderPool; + this.provider = new GifBitmapProvider(bitmapPool); + this.parserPool = parserPool; + } + + @Override + public GifDrawableResource decode(InputStream source, int width, int height) { + byte[] data = inputStreamToBytes(source); + final GifHeaderParser parser = parserPool.obtain(data); + final GifDecoder decoder = decoderPool.obtain(provider); + try { + return decode(data, width, height, parser, decoder); + } finally { + parserPool.release(parser); + decoderPool.release(decoder); + } + } + + private GifDrawableResource decode(byte[] data, int width, int height, GifHeaderParser parser, GifDecoder decoder) { + final GifHeader header = parser.parseHeader(); + if (header.getNumFrames() <= 0 || header.getStatus() != GifDecoder.STATUS_OK) { + // If we couldn't decode the GIF, we will end up with a frame count of 0. + return null; + } + + Bitmap firstFrame = decodeFirstFrame(decoder, header, data); + if (firstFrame == null) { + return null; + } + + Transformation unitTransformation = UnitTransformation.get(); + + GifDrawable gifDrawable = new GifDrawable(context, provider, bitmapPool, unitTransformation, width, height, + header, data, firstFrame); + + return new GifDrawableResource(gifDrawable); + } + + private Bitmap decodeFirstFrame(GifDecoder decoder, GifHeader header, byte[] data) { + decoder.setData(header, data); + decoder.advance(); + return decoder.getNextFrame(); + } + + @Override + public String getId() { + return ""; + } + + private static byte[] inputStreamToBytes(InputStream is) { + final int bufferSize = 16384; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(bufferSize); + try { + int nRead; + byte[] data = new byte[bufferSize]; + while ((nRead = is.read(data)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + } catch (IOException e) { + Log.w(TAG, "Error reading data from stream", e); + } + //TODO the returned byte[] may be partial if an IOException was thrown from read + return buffer.toByteArray(); + } + + // Visible for testing. + static class GifDecoderPool { + private final Queue pool = Util.createQueue(0); + + public synchronized GifDecoder obtain(GifDecoder.BitmapProvider bitmapProvider) { + GifDecoder result = pool.poll(); + if (result == null) { + result = new GifDecoder(bitmapProvider); + } + return result; + } + + public synchronized void release(GifDecoder decoder) { + decoder.clear(); + pool.offer(decoder); + } + } + + // Visible for testing. + static class GifHeaderParserPool { + private final Queue pool = Util.createQueue(0); + + public synchronized GifHeaderParser obtain(byte[] data) { + GifHeaderParser result = pool.poll(); + if (result == null) { + result = new GifHeaderParser(); + } + return result.setData(data); + } + + public synchronized void release(GifHeaderParser parser) { + parser.clear(); + pool.offer(parser); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceEncoder.java new file mode 100755 index 0000000..02d413c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gif/GifResourceEncoder.java @@ -0,0 +1,149 @@ +package com.example.bumptech.glide.load.resource.gif; + +import android.graphics.Bitmap; +import android.util.Log; + + +import com.example.bumptech.glide.gifdecoder.GifDecoder; +import com.example.bumptech.glide.gifdecoder.GifHeader; +import com.example.bumptech.glide.gifdecoder.GifHeaderParser; +import com.example.bumptech.glide.gifencoder.AnimatedGifEncoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.UnitTransformation; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; +import com.example.bumptech.glide.util.LogTime; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An {@link ResourceEncoder} that can write + * {@link GifDrawable} to cache. + */ +public class GifResourceEncoder implements ResourceEncoder { + private static final Factory FACTORY = new Factory(); + private static final String TAG = "GifEncoder"; + private final GifDecoder.BitmapProvider provider; + private final BitmapPool bitmapPool; + private final Factory factory; + + public GifResourceEncoder(BitmapPool bitmapPool) { + this(bitmapPool, FACTORY); + } + + // Visible for testing. + GifResourceEncoder(BitmapPool bitmapPool, Factory factory) { + this.bitmapPool = bitmapPool; + provider = new GifBitmapProvider(bitmapPool); + this.factory = factory; + } + + @Override + public boolean encode(Resource resource, OutputStream os) { + long startTime = LogTime.getLogTime(); + + GifDrawable drawable = resource.get(); + Transformation transformation = drawable.getFrameTransformation(); + if (transformation instanceof UnitTransformation) { + return writeDataDirect(drawable.getData(), os); + } + + GifDecoder decoder = decodeHeaders(drawable.getData()); + + AnimatedGifEncoder encoder = factory.buildEncoder(); + if (!encoder.start(os)) { + return false; + } + + for (int i = 0; i < decoder.getFrameCount(); i++) { + Bitmap currentFrame = decoder.getNextFrame(); + Resource transformedResource = getTransformedFrame(currentFrame, transformation, drawable); + try { + if (!encoder.addFrame(transformedResource.get())) { + return false; + } + int currentFrameIndex = decoder.getCurrentFrameIndex(); + int delay = decoder.getDelay(currentFrameIndex); + encoder.setDelay(delay); + + decoder.advance(); + } finally { + transformedResource.recycle(); + } + } + + boolean result = encoder.finish(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Encoded gif with " + decoder.getFrameCount() + " frames and " + drawable.getData().length + + " bytes in " + LogTime.getElapsedMillis(startTime) + " ms"); + } + + return result; + } + + private boolean writeDataDirect(byte[] data, OutputStream os) { + boolean success = true; + try { + os.write(data); + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Failed to write data to output stream in GifResourceEncoder", e); + } + success = false; + } + return success; + } + + private GifDecoder decodeHeaders(byte[] data) { + GifHeaderParser parser = factory.buildParser(); + parser.setData(data); + GifHeader header = parser.parseHeader(); + + GifDecoder decoder = factory.buildDecoder(provider); + decoder.setData(header, data); + decoder.advance(); + + return decoder; + } + + private Resource getTransformedFrame(Bitmap currentFrame, Transformation transformation, + GifDrawable drawable) { + // TODO: what if current frame is null? + Resource bitmapResource = factory.buildFrameResource(currentFrame, bitmapPool); + Resource transformedResource = transformation.transform(bitmapResource, + drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + if (!bitmapResource.equals(transformedResource)) { + bitmapResource.recycle(); + } + return transformedResource; + } + + @Override + public String getId() { + return ""; + } + + // Visible for testing. + static class Factory { + + public GifDecoder buildDecoder(GifDecoder.BitmapProvider bitmapProvider) { + return new GifDecoder(bitmapProvider); + } + + public GifHeaderParser buildParser() { + return new GifHeaderParser(); + } + + public AnimatedGifEncoder buildEncoder() { + return new AnimatedGifEncoder(); + } + + public Resource buildFrameResource(Bitmap bitmap, BitmapPool bitmapPool) { + return new BitmapResource(bitmap, bitmapPool); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapper.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapper.java new file mode 100755 index 0000000..c373806 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapper.java @@ -0,0 +1,52 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; + + +/** + * A wrapper that contains either an {@link Bitmap} resource or an + * {@link GifDrawable} resource. + */ +public class GifBitmapWrapper { + private final Resource gifResource; + private final Resource bitmapResource; + + public GifBitmapWrapper(Resource bitmapResource, Resource gifResource) { + if (bitmapResource != null && gifResource != null) { + throw new IllegalArgumentException("Can only contain either a bitmap resource or a gif resource, not both"); + } + if (bitmapResource == null && gifResource == null) { + throw new IllegalArgumentException("Must contain either a bitmap resource or a gif resource"); + } + this.bitmapResource = bitmapResource; + this.gifResource = gifResource; + } + + /** + * Returns the size of the wrapped resource. + */ + public int getSize() { + if (bitmapResource != null) { + return bitmapResource.getSize(); + } else { + return gifResource.getSize(); + } + } + + /** + * Returns the wrapped {@link Bitmap} resource if it exists, or null. + */ + public Resource getBitmapResource() { + return bitmapResource; + } + + /** + * Returns the wrapped {@link GifDrawable} resource if it exists, or null. + */ + public Resource getGifResource() { + return gifResource; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResource.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResource.java new file mode 100755 index 0000000..7fca6ee --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResource.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; + + +/** + * A resource that wraps an {@link GifBitmapWrapper}. + */ +public class GifBitmapWrapperResource implements Resource { + private final GifBitmapWrapper data; + + public GifBitmapWrapperResource(GifBitmapWrapper data) { + if (data == null) { + throw new NullPointerException("Data must not be null"); + } + this.data = data; + } + + @Override + public GifBitmapWrapper get() { + return data; + } + + @Override + public int getSize() { + return data.getSize(); + } + + @Override + public void recycle() { + Resource bitmapResource = data.getBitmapResource(); + if (bitmapResource != null) { + bitmapResource.recycle(); + } + Resource gifDataResource = data.getGifResource(); + if (gifDataResource != null) { + gifDataResource.recycle(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceDecoder.java new file mode 100755 index 0000000..2906c1f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceDecoder.java @@ -0,0 +1,151 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; +import com.example.bumptech.glide.load.resource.bitmap.ImageHeaderParser; +import com.example.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.util.ByteArrayPool; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An {@link ResourceDecoder} that can decode either an {@link Bitmap} or an {@link GifDrawable} + * from an {@link InputStream} or a {@link android.os.ParcelFileDescriptor ParcelFileDescriptor}. + */ +public class GifBitmapWrapperResourceDecoder implements ResourceDecoder { + private static final ImageTypeParser DEFAULT_PARSER = new ImageTypeParser(); + private static final BufferedStreamFactory DEFAULT_STREAM_FACTORY = new BufferedStreamFactory(); + // 2048 is rather arbitrary, for most well formatted image types we only need 32 bytes. + // Visible for testing. + static final int MARK_LIMIT_BYTES = 2048; + + private final ResourceDecoder bitmapDecoder; + private final ResourceDecoder gifDecoder; + private final BitmapPool bitmapPool; + private final ImageTypeParser parser; + private final BufferedStreamFactory streamFactory; + private String id; + + public GifBitmapWrapperResourceDecoder(ResourceDecoder bitmapDecoder, + ResourceDecoder gifDecoder, BitmapPool bitmapPool) { + this(bitmapDecoder, gifDecoder, bitmapPool, DEFAULT_PARSER, DEFAULT_STREAM_FACTORY); + } + + // Visible for testing. + GifBitmapWrapperResourceDecoder(ResourceDecoder bitmapDecoder, + ResourceDecoder gifDecoder, BitmapPool bitmapPool, ImageTypeParser parser, + BufferedStreamFactory streamFactory) { + this.bitmapDecoder = bitmapDecoder; + this.gifDecoder = gifDecoder; + this.bitmapPool = bitmapPool; + this.parser = parser; + this.streamFactory = streamFactory; + } + + @SuppressWarnings("resource") + // @see ResourceDecoder.decode + @Override + public Resource decode(ImageVideoWrapper source, int width, int height) throws IOException { + ByteArrayPool pool = ByteArrayPool.get(); + byte[] tempBytes = pool.getBytes(); + + GifBitmapWrapper wrapper = null; + try { + wrapper = decode(source, width, height, tempBytes); + } finally { + pool.releaseBytes(tempBytes); + } + return wrapper != null ? new GifBitmapWrapperResource(wrapper) : null; + } + + private GifBitmapWrapper decode(ImageVideoWrapper source, int width, int height, byte[] bytes) throws IOException { + final GifBitmapWrapper result; + if (source.getStream() != null) { + result = decodeStream(source, width, height, bytes); + } else { + result = decodeBitmapWrapper(source, width, height); + } + return result; + } + + private GifBitmapWrapper decodeStream(ImageVideoWrapper source, int width, int height, byte[] bytes) + throws IOException { + InputStream bis = streamFactory.build(source.getStream(), bytes); + bis.mark(MARK_LIMIT_BYTES); + ImageHeaderParser.ImageType type = parser.parse(bis); + bis.reset(); + + GifBitmapWrapper result = null; + if (type == ImageHeaderParser.ImageType.GIF) { + result = decodeGifWrapper(bis, width, height); + } + // Decoding the gif may fail even if the type matches. + if (result == null) { + // We can only reset the buffered InputStream, so to start from the beginning of the stream, we need to + // pass in a new source containing the buffered stream rather than the original stream. + ImageVideoWrapper forBitmapDecoder = new ImageVideoWrapper(bis, source.getFileDescriptor()); + result = decodeBitmapWrapper(forBitmapDecoder, width, height); + } + return result; + } + + private GifBitmapWrapper decodeGifWrapper(InputStream bis, int width, int height) throws IOException { + GifBitmapWrapper result = null; + Resource gifResource = gifDecoder.decode(bis, width, height); + if (gifResource != null) { + GifDrawable drawable = gifResource.get(); + // We can more efficiently hold Bitmaps in memory, so for static GIFs, try to return Bitmaps + // instead. Returning a Bitmap incurs the cost of allocating the GifDrawable as well as the normal + // Bitmap allocation, but since we can encode the Bitmap out as a JPEG, future decodes will be + // efficient. + if (drawable.getFrameCount() > 1) { + result = new GifBitmapWrapper(null /*bitmapResource*/, gifResource); + } else { + Resource bitmapResource = new BitmapResource(drawable.getFirstFrame(), bitmapPool); + result = new GifBitmapWrapper(bitmapResource, null /*gifResource*/); + } + } + return result; + } + + private GifBitmapWrapper decodeBitmapWrapper(ImageVideoWrapper toDecode, int width, int height) throws IOException { + GifBitmapWrapper result = null; + + Resource bitmapResource = bitmapDecoder.decode(toDecode, width, height); + if (bitmapResource != null) { + result = new GifBitmapWrapper(bitmapResource, null); + } + + return result; + } + + @Override + public String getId() { + if (id == null) { + id = gifDecoder.getId() + bitmapDecoder.getId(); + } + return id; + } + + // Visible for testing. + static class BufferedStreamFactory { + public InputStream build(InputStream is, byte[] buffer) { + return new RecyclableBufferedInputStream(is, buffer); + } + } + + // Visible for testing. + static class ImageTypeParser { + public ImageHeaderParser.ImageType parse(InputStream is) throws IOException { + return new ImageHeaderParser(is).getType(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceEncoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceEncoder.java new file mode 100755 index 0000000..b092eb7 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperResourceEncoder.java @@ -0,0 +1,46 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; + +import java.io.OutputStream; + +/** + * A {@link ResourceEncoder} that can encode either an {@link Bitmap} or + * {@link GifDrawable}. + */ +public class GifBitmapWrapperResourceEncoder implements ResourceEncoder { + private final ResourceEncoder bitmapEncoder; + private final ResourceEncoder gifEncoder; + private String id; + + public GifBitmapWrapperResourceEncoder(ResourceEncoder bitmapEncoder, + ResourceEncoder gifEncoder) { + this.bitmapEncoder = bitmapEncoder; + this.gifEncoder = gifEncoder; + } + + @Override + public boolean encode(Resource resource, OutputStream os) { + final GifBitmapWrapper gifBitmap = resource.get(); + final Resource bitmapResource = gifBitmap.getBitmapResource(); + + if (bitmapResource != null) { + return bitmapEncoder.encode(bitmapResource, os); + } else { + return gifEncoder.encode(gifBitmap.getGifResource(), os); + } + } + + @Override + public String getId() { + if (id == null) { + id = bitmapEncoder.getId() + gifEncoder.getId(); + } + return id; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperStreamResourceDecoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperStreamResourceDecoder.java new file mode 100755 index 0000000..c938418 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperStreamResourceDecoder.java @@ -0,0 +1,32 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + + +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link ResourceDecoder} that can decode an + * {@link GifBitmapWrapper} from {@link InputStream} data. + */ +public class GifBitmapWrapperStreamResourceDecoder implements ResourceDecoder { + private final ResourceDecoder gifBitmapDecoder; + + public GifBitmapWrapperStreamResourceDecoder( + ResourceDecoder gifBitmapDecoder) { + this.gifBitmapDecoder = gifBitmapDecoder; + } + + @Override + public Resource decode(InputStream source, int width, int height) throws IOException { + return gifBitmapDecoder.decode(new ImageVideoWrapper(source, null), width, height); + } + + @Override + public String getId() { + return gifBitmapDecoder.getId(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperTransformation.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperTransformation.java new file mode 100755 index 0000000..21424f1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/GifBitmapWrapperTransformation.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.load.resource.gif.GifDrawableTransformation; + + +/** + * A {@link Transformation} that can apply a wrapped {@link Bitmap} + * transformation to both {@link Bitmap}s and {@link GifDrawable}. + */ +public class GifBitmapWrapperTransformation implements Transformation { + private final Transformation bitmapTransformation; + private final Transformation gifDataTransformation; + + public GifBitmapWrapperTransformation(BitmapPool bitmapPool, Transformation bitmapTransformation) { + this(bitmapTransformation, new GifDrawableTransformation(bitmapTransformation, bitmapPool)); + } + + GifBitmapWrapperTransformation(Transformation bitmapTransformation, + Transformation gifDataTransformation) { + this.bitmapTransformation = bitmapTransformation; + this.gifDataTransformation = gifDataTransformation; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + Resource bitmapResource = resource.get().getBitmapResource(); + Resource gifResource = resource.get().getGifResource(); + if (bitmapResource != null && bitmapTransformation != null) { + Resource transformed = bitmapTransformation.transform(bitmapResource, outWidth, outHeight); + if (!bitmapResource.equals(transformed)) { + GifBitmapWrapper gifBitmap = new GifBitmapWrapper(transformed, resource.get().getGifResource()); + return new GifBitmapWrapperResource(gifBitmap); + } + } else if (gifResource != null && gifDataTransformation != null) { + Resource transformed = gifDataTransformation.transform(gifResource, outWidth, outHeight); + if (!gifResource.equals(transformed)) { + GifBitmapWrapper gifBitmap = new GifBitmapWrapper(resource.get().getBitmapResource(), transformed); + return new GifBitmapWrapperResource(gifBitmap); + } + } + return resource; + } + + @Override + public String getId() { + return bitmapTransformation.getId(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/ImageVideoGifDrawableLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/ImageVideoGifDrawableLoadProvider.java new file mode 100755 index 0000000..d9b325b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/gifbitmap/ImageVideoGifDrawableLoadProvider.java @@ -0,0 +1,64 @@ +package com.example.bumptech.glide.load.resource.gifbitmap; + +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.model.ImageVideoWrapper; +import com.example.bumptech.glide.load.resource.file.FileToStreamDecoder; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; +import com.example.bumptech.glide.provider.DataLoadProvider; + +import java.io.File; +import java.io.InputStream; + +/** + * An {@link DataLoadProvider} that can load either an + * {@link GifDrawable} or an {@link Bitmap} from either an + * {@link InputStream} or an {@link android.os.ParcelFileDescriptor}. + */ +public class ImageVideoGifDrawableLoadProvider implements DataLoadProvider { + private final ResourceDecoder cacheDecoder; + private final ResourceDecoder sourceDecoder; + private final ResourceEncoder encoder; + private final Encoder sourceEncoder; + + public ImageVideoGifDrawableLoadProvider(DataLoadProvider bitmapProvider, + DataLoadProvider gifProvider, BitmapPool bitmapPool) { + + final GifBitmapWrapperResourceDecoder decoder = new GifBitmapWrapperResourceDecoder( + bitmapProvider.getSourceDecoder(), + gifProvider.getSourceDecoder(), + bitmapPool + ); + cacheDecoder = new FileToStreamDecoder(new GifBitmapWrapperStreamResourceDecoder(decoder)); + sourceDecoder = decoder; + encoder = new GifBitmapWrapperResourceEncoder(bitmapProvider.getEncoder(), gifProvider.getEncoder()); + + //TODO: what about the gif provider? + sourceEncoder = bitmapProvider.getSourceEncoder(); + } + + @Override + public ResourceDecoder getCacheDecoder() { + return cacheDecoder; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return sourceDecoder; + } + + @Override + public Encoder getSourceEncoder() { + return sourceEncoder; + } + + @Override + public ResourceEncoder getEncoder() { + return encoder; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapBytesTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapBytesTranscoder.java new file mode 100755 index 0000000..e4c3667 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapBytesTranscoder.java @@ -0,0 +1,41 @@ +package com.example.bumptech.glide.load.resource.transcode; + +import android.graphics.Bitmap; + + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.bytes.BytesResource; + +import java.io.ByteArrayOutputStream; + +/** + * An {@link ResourceTranscoder} that converts + * {@link Bitmap}s into byte arrays using + * {@link Bitmap#compress(Bitmap.CompressFormat, int, java.io.OutputStream)}. + */ +public class BitmapBytesTranscoder implements ResourceTranscoder { + private final Bitmap.CompressFormat compressFormat; + private final int quality; + + public BitmapBytesTranscoder() { + this(Bitmap.CompressFormat.JPEG, 100); + } + + public BitmapBytesTranscoder(Bitmap.CompressFormat compressFormat, int quality) { + this.compressFormat = compressFormat; + this.quality = quality; + } + + @Override + public Resource transcode(Resource toTranscode) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + toTranscode.get().compress(compressFormat, quality, os); + toTranscode.recycle(); + return new BytesResource(os.toByteArray()); + } + + @Override + public String getId() { + return "BitmapBytesTranscoder.com.bumptech.glide.load.resource.transcode"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapToGlideDrawableTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapToGlideDrawableTranscoder.java new file mode 100755 index 0000000..4e12727 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/BitmapToGlideDrawableTranscoder.java @@ -0,0 +1,40 @@ +package com.example.bumptech.glide.load.resource.transcode; + +import android.content.Context; +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; + + +/** + * A wrapper for {@link GlideBitmapDrawableTranscoder} that transcodes + * to {@link GlideDrawable} rather than + * {@link com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable}. + * + * TODO: use ? extends GlideDrawable rather than GlideDrawable directly and remove this class. + */ +public class BitmapToGlideDrawableTranscoder implements ResourceTranscoder { + + private final GlideBitmapDrawableTranscoder glideBitmapDrawableTranscoder; + + public BitmapToGlideDrawableTranscoder(Context context) { + this(new GlideBitmapDrawableTranscoder(context)); + } + + public BitmapToGlideDrawableTranscoder(GlideBitmapDrawableTranscoder glideBitmapDrawableTranscoder) { + this.glideBitmapDrawableTranscoder = glideBitmapDrawableTranscoder; + } + + @SuppressWarnings("unchecked") + @Override + public Resource transcode(Resource toTranscode) { + return (Resource) (Resource) + glideBitmapDrawableTranscoder.transcode(toTranscode); + } + + @Override + public String getId() { + return glideBitmapDrawableTranscoder.getId(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifBitmapWrapperDrawableTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifBitmapWrapperDrawableTranscoder.java new file mode 100755 index 0000000..7974620 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifBitmapWrapperDrawableTranscoder.java @@ -0,0 +1,44 @@ +package com.example.bumptech.glide.load.resource.transcode; + +import android.graphics.Bitmap; + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable; +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.example.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapper; + + +/** + * An {@link ResourceTranscoder} that can transcode either an + * {@link Bitmap} or an {@link com.bumptech.glide.load.resource.gif.GifDrawable} into an + * {@link android.graphics.drawable.Drawable}. + */ +public class GifBitmapWrapperDrawableTranscoder implements ResourceTranscoder { + private final ResourceTranscoder bitmapDrawableResourceTranscoder; + + public GifBitmapWrapperDrawableTranscoder( + ResourceTranscoder bitmapDrawableResourceTranscoder) { + this.bitmapDrawableResourceTranscoder = bitmapDrawableResourceTranscoder; + } + + @SuppressWarnings("unchecked") + @Override + public Resource transcode(Resource toTranscode) { + GifBitmapWrapper gifBitmap = toTranscode.get(); + Resource bitmapResource = gifBitmap.getBitmapResource(); + + final Resource result; + if (bitmapResource != null) { + result = bitmapDrawableResourceTranscoder.transcode(bitmapResource); + } else { + result = gifBitmap.getGifResource(); + } + // This is unchecked but always safe, anything that extends a Drawable can be safely cast to a Drawable. + return (Resource) result; + } + + @Override + public String getId() { + return "GifBitmapWrapperDrawableTranscoder.com.bumptech.glide.load.resource.transcode"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoder.java new file mode 100755 index 0000000..50fe8f1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GifDrawableBytesTranscoder.java @@ -0,0 +1,24 @@ +package com.example.bumptech.glide.load.resource.transcode; + + +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.resource.bytes.BytesResource; +import com.example.bumptech.glide.load.resource.gif.GifDrawable; + +/** + * An {@link ResourceTranscoder} that converts + * {@link GifDrawable} into bytes by obtaining the original bytes of the GIF from + * the {@link GifDrawable}. + */ +public class GifDrawableBytesTranscoder implements ResourceTranscoder { + @Override + public Resource transcode(Resource toTranscode) { + GifDrawable gifData = toTranscode.get(); + return new BytesResource(gifData.getData()); + } + + @Override + public String getId() { + return "GifDrawableBytesTranscoder.com.bumptech.glide.load.resource.transcode"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GlideBitmapDrawableTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GlideBitmapDrawableTranscoder.java new file mode 100755 index 0000000..0ad0373 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/GlideBitmapDrawableTranscoder.java @@ -0,0 +1,41 @@ +package com.example.bumptech.glide.load.resource.transcode; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable; +import com.example.bumptech.glide.load.resource.bitmap.GlideBitmapDrawableResource; + + +/** + * An {@link ResourceTranscoder} that converts + * {@link Bitmap}s into {@link android.graphics.drawable.BitmapDrawable}s. + */ +public class GlideBitmapDrawableTranscoder implements ResourceTranscoder { + private final Resources resources; + private final BitmapPool bitmapPool; + + public GlideBitmapDrawableTranscoder(Context context) { + this(context.getResources(), Glide.get(context).getBitmapPool()); + } + + public GlideBitmapDrawableTranscoder(Resources resources, BitmapPool bitmapPool) { + this.resources = resources; + this.bitmapPool = bitmapPool; + } + + @Override + public Resource transcode(Resource toTranscode) { + GlideBitmapDrawable drawable = new GlideBitmapDrawable(resources, toTranscode.get()); + return new GlideBitmapDrawableResource(drawable, bitmapPool); + } + + @Override + public String getId() { + return "GlideBitmapDrawableTranscoder.com.bumptech.glide.load.resource.transcode"; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/ResourceTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/ResourceTranscoder.java new file mode 100755 index 0000000..cdedb66 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/ResourceTranscoder.java @@ -0,0 +1,22 @@ +package com.example.bumptech.glide.load.resource.transcode; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * Transcodes a resource of one type to a resource of another type. + * + * @param The type of the resource that will be transcoded from. + * @param The type of the resource that will be transcoded to. + */ +public interface ResourceTranscoder { + + /** + * Transcodes the given resource to the new resource type and returns the wew resource. + * + * @param toTranscode The resource to transcode. + */ + Resource transcode(Resource toTranscode); + + String getId(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/TranscoderRegistry.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/TranscoderRegistry.java new file mode 100755 index 0000000..2e24e8c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/TranscoderRegistry.java @@ -0,0 +1,59 @@ +package com.example.bumptech.glide.load.resource.transcode; + + +import com.example.bumptech.glide.util.MultiClassKey; + +import java.util.HashMap; +import java.util.Map; + +/** + * A class that allows {@link ResourceTranscoder}s to be registered and + * retrieved by the classes they convert between. + */ +public class TranscoderRegistry { + private static final MultiClassKey GET_KEY = new MultiClassKey(); + + private final Map> factories = + new HashMap>(); + + /** + * Registers the given {@link ResourceTranscoder} using the given + * classes so it can later be retrieved using the given classes. + * + * @param decodedClass The class of the resource that the transcoder transcodes from. + * @param transcodedClass The class of the resource that the transcoder transcodes to. + * @param transcoder The transcoder. + * @param The type of the resource that the transcoder transcodes from. + * @param The type of the resource that the transcoder transcodes to. + */ + public void register(Class decodedClass, Class transcodedClass, ResourceTranscoder transcoder) { + factories.put(new MultiClassKey(decodedClass, transcodedClass), transcoder); + } + + /** + * Returns the currently registered {@link ResourceTranscoder} for the + * given classes. + * + * @param decodedClass The class of the resource that the transcoder transcodes from. + * @param transcodedClass The class of the resource that the transcoder transcodes to. + * @param The type of the resource that the transcoder transcodes from. + * @param The type of the resource that the transcoder transcodes to. + */ + @SuppressWarnings("unchecked") + public ResourceTranscoder get(Class decodedClass, Class transcodedClass) { + if (decodedClass.equals(transcodedClass)) { + // we know they're the same type (Z and R) + return (ResourceTranscoder) UnitTranscoder.get(); + } + final ResourceTranscoder result; + synchronized (GET_KEY) { + GET_KEY.set(decodedClass, transcodedClass); + result = factories.get(GET_KEY); + } + if (result == null) { + throw new IllegalArgumentException("No transcoder registered for " + decodedClass + " and " + + transcodedClass); + } + return (ResourceTranscoder) result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/UnitTranscoder.java b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/UnitTranscoder.java new file mode 100755 index 0000000..774f75e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/load/resource/transcode/UnitTranscoder.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.load.resource.transcode; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A simple {@link ResourceTranscoder} that simply returns the given resource. + * + * @param The type of the resource that will be transcoded from and to. + */ +public class UnitTranscoder implements ResourceTranscoder { + private static final UnitTranscoder UNIT_TRANSCODER = new UnitTranscoder(); + + @SuppressWarnings("unchecked") + public static ResourceTranscoder get() { + return (ResourceTranscoder) UNIT_TRANSCODER; + } + + @Override + public Resource transcode(Resource toTranscode) { + return toTranscode; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/ActivityFragmentLifecycle.java b/core/src/main/java/com/example/bumptech/glide/manager/ActivityFragmentLifecycle.java new file mode 100755 index 0000000..047cf49 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/ActivityFragmentLifecycle.java @@ -0,0 +1,67 @@ +package com.example.bumptech.glide.manager; + +import com.example.bumptech.glide.util.Util; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * A {@link Lifecycle} implementation for tracking and notifying listeners of + * {@link android.app.Fragment} and {@link android.app.Activity} lifecycle events. + */ +class ActivityFragmentLifecycle implements Lifecycle { + private final Set lifecycleListeners = + Collections.newSetFromMap(new WeakHashMap()); + private boolean isStarted; + private boolean isDestroyed; + + /** + * Adds the given listener to the list of listeners to be notified on each lifecycle event. + * + *

+ * The latest lifecycle event will be called on the given listener synchronously in this method. If the + * activity or fragment is stopped, {@link LifecycleListener#onStop()}} will be called, and same for onStart and + * onDestroy. + *

+ * + *

+ * Note - {@link LifecycleListener}s that are added more than once will have their + * lifecycle methods called more than once. It is the caller's responsibility to avoid adding listeners + * multiple times. + *

+ */ + @Override + public void addListener(LifecycleListener listener) { + lifecycleListeners.add(listener); + + if (isDestroyed) { + listener.onDestroy(); + } else if (isStarted) { + listener.onStart(); + } else { + listener.onStop(); + } + } + + void onStart() { + isStarted = true; + for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { + lifecycleListener.onStart(); + } + } + + void onStop() { + isStarted = false; + for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { + lifecycleListener.onStop(); + } + } + + void onDestroy() { + isDestroyed = true; + for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { + lifecycleListener.onDestroy(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/ApplicationLifecycle.java b/core/src/main/java/com/example/bumptech/glide/manager/ApplicationLifecycle.java new file mode 100755 index 0000000..4c1ad60 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/ApplicationLifecycle.java @@ -0,0 +1,17 @@ +package com.example.bumptech.glide.manager; + +/** + * A {@link Lifecycle} implementation for tracking and notifying listeners of + * {@link android.app.Application} lifecycle events. + * + *

+ * Since there are essentially no {@link android.app.Application} lifecycle events, this class simply defaults to + * notifying new listeners that they are started. + *

+ */ +class ApplicationLifecycle implements Lifecycle { + @Override + public void addListener(LifecycleListener listener) { + listener.onStart(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitor.java b/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitor.java new file mode 100755 index 0000000..756023d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitor.java @@ -0,0 +1,19 @@ +package com.example.bumptech.glide.manager; + +/** + * An interface for monitoring network connectivity events. + */ +public interface ConnectivityMonitor extends LifecycleListener { + + /** + * An interface for listening to network connectivity events picked up by the monitor. + */ + interface ConnectivityListener { + /** + * Called when the connectivity state changes. + * + * @param isConnected True if we're currently connected to a network, false otherwise. + */ + void onConnectivityChanged(boolean isConnected); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitorFactory.java b/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitorFactory.java new file mode 100755 index 0000000..7e8101b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/ConnectivityMonitorFactory.java @@ -0,0 +1,21 @@ +package com.example.bumptech.glide.manager; + +import android.content.Context; +import android.content.pm.PackageManager; + +/** + * A factory class that produces a functional {@link ConnectivityMonitor} if the application + * has the {@code android.permission.ACCESS_NETWORK_STATE} permission and a no-op non functional + * {@link ConnectivityMonitor} if the app does not have the required permission. + */ +public class ConnectivityMonitorFactory { + public ConnectivityMonitor build(Context context, ConnectivityMonitor.ConnectivityListener listener) { + final int res = context.checkCallingOrSelfPermission("android.permission.ACCESS_NETWORK_STATE"); + final boolean hasPermission = res == PackageManager.PERMISSION_GRANTED; + if (hasPermission) { + return new DefaultConnectivityMonitor(context, listener); + } else { + return new NullConnectivityMonitor(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/DefaultConnectivityMonitor.java b/core/src/main/java/com/example/bumptech/glide/manager/DefaultConnectivityMonitor.java new file mode 100755 index 0000000..9a72c6c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/DefaultConnectivityMonitor.java @@ -0,0 +1,73 @@ +package com.example.bumptech.glide.manager; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +class DefaultConnectivityMonitor implements ConnectivityMonitor { + private final Context context; + private final ConnectivityListener listener; + + private boolean isConnected; + private boolean isRegistered; + + private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + boolean wasConnected = isConnected; + isConnected = isConnected(context); + if (wasConnected != isConnected) { + listener.onConnectivityChanged(isConnected); + } + } + }; + + public DefaultConnectivityMonitor(Context context, ConnectivityListener listener) { + this.context = context.getApplicationContext(); + this.listener = listener; + } + + private void register() { + if (isRegistered) { + return; + } + + isConnected = isConnected(context); + context.registerReceiver(connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + isRegistered = true; + } + + private void unregister() { + if (!isRegistered) { + return; + } + + context.unregisterReceiver(connectivityReceiver); + isRegistered = false; + } + + private boolean isConnected(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnected(); + } + + @Override + public void onStart() { + register(); + } + + @Override + public void onStop() { + unregister(); + } + + @Override + public void onDestroy() { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/EmptyRequestManagerTreeNode.java b/core/src/main/java/com/example/bumptech/glide/manager/EmptyRequestManagerTreeNode.java new file mode 100755 index 0000000..672ac64 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/EmptyRequestManagerTreeNode.java @@ -0,0 +1,17 @@ +package com.example.bumptech.glide.manager; + + +import com.example.bumptech.glide.RequestManager; + +import java.util.Collections; +import java.util.Set; + +/** + * A {@link RequestManagerTreeNode} that returns no relatives. + */ +final class EmptyRequestManagerTreeNode implements RequestManagerTreeNode { + @Override + public Set getDescendants() { + return Collections.emptySet(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/Lifecycle.java b/core/src/main/java/com/example/bumptech/glide/manager/Lifecycle.java new file mode 100755 index 0000000..a76f070 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/Lifecycle.java @@ -0,0 +1,11 @@ +package com.example.bumptech.glide.manager; + +/** + * An interface for listening to Activity/Fragment lifecycle events. + */ +public interface Lifecycle { + /** + * Adds the given listener to the set of listeners managed by this Lifecycle implementation. + */ + void addListener(LifecycleListener listener); +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/LifecycleListener.java b/core/src/main/java/com/example/bumptech/glide/manager/LifecycleListener.java new file mode 100755 index 0000000..3a783f6 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/LifecycleListener.java @@ -0,0 +1,23 @@ +package com.example.bumptech.glide.manager; + +/** + * An interface for listener to {@link android.app.Fragment} and {@link android.app.Activity} lifecycle events. + */ +public interface LifecycleListener { + + /** + * Callback for when {@link android.app.Fragment#onStart()}} or {@link android.app.Activity#onStart()} is called. + */ + void onStart(); + + /** + * Callback for when {@link android.app.Fragment#onStop()}} or {@link android.app.Activity#onStop()}} is called. + */ + void onStop(); + + /** + * Callback for when {@link android.app.Fragment#onDestroy()}} or {@link android.app.Activity#onDestroy()} is + * called. + */ + void onDestroy(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/NullConnectivityMonitor.java b/core/src/main/java/com/example/bumptech/glide/manager/NullConnectivityMonitor.java new file mode 100755 index 0000000..1539d6b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/NullConnectivityMonitor.java @@ -0,0 +1,22 @@ +package com.example.bumptech.glide.manager; + +/** + * A no-op {@link ConnectivityMonitor}. + */ +class NullConnectivityMonitor implements ConnectivityMonitor { + + @Override + public void onStart() { + // Do nothing. + } + + @Override + public void onStop() { + // Do nothing. + } + + @Override + public void onDestroy() { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerFragment.java b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerFragment.java new file mode 100755 index 0000000..d11ab8b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerFragment.java @@ -0,0 +1,184 @@ +package com.example.bumptech.glide.manager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Fragment; +import android.os.Build; + + +import com.example.bumptech.glide.RequestManager; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * A view-less {@link Fragment} used to safely store an {@link RequestManager} that + * can be used to start, stop and manage Glide requests started for targets the fragment or activity this fragment is a + * child of. + * + * @see SupportRequestManagerFragment + * @see RequestManagerRetriever + * @see RequestManager + */ +@TargetApi(Build.VERSION_CODES.HONEYCOMB) +public class RequestManagerFragment extends Fragment { + private final ActivityFragmentLifecycle lifecycle; + private final RequestManagerTreeNode requestManagerTreeNode = new FragmentRequestManagerTreeNode(); + private RequestManager requestManager; + private final HashSet childRequestManagerFragments + = new HashSet(); + private RequestManagerFragment rootRequestManagerFragment; + + public RequestManagerFragment() { + this(new ActivityFragmentLifecycle()); + } + + // For testing only. + @SuppressLint("ValidFragment") + RequestManagerFragment(ActivityFragmentLifecycle lifecycle) { + this.lifecycle = lifecycle; + } + + /** + * Sets the current {@link RequestManager}. + * + * @param requestManager The request manager to use. + */ + public void setRequestManager(RequestManager requestManager) { + this.requestManager = requestManager; + } + + ActivityFragmentLifecycle getLifecycle() { + return lifecycle; + } + + /** + * Returns the current {@link RequestManager} or null if none exists. + */ + public RequestManager getRequestManager() { + return requestManager; + } + + public RequestManagerTreeNode getRequestManagerTreeNode() { + return requestManagerTreeNode; + } + + private void addChildRequestManagerFragment(RequestManagerFragment child) { + childRequestManagerFragments.add(child); + } + + private void removeChildRequestManagerFragment(RequestManagerFragment child) { + childRequestManagerFragments.remove(child); + } + + /** + * Returns the set of fragments that this RequestManagerFragment's parent is a parent to. (i.e. our parent is + * the fragment that we are annotating). + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public Set getDescendantRequestManagerFragments() { + if (rootRequestManagerFragment == this) { + return Collections.unmodifiableSet(childRequestManagerFragments); + } else if (rootRequestManagerFragment == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Pre JB MR1 doesn't allow us to get the parent fragment so we can't introspect hierarchy, so just + // return an empty set. + return Collections.emptySet(); + } else { + HashSet descendants = new HashSet(); + for (RequestManagerFragment fragment + : rootRequestManagerFragment.getDescendantRequestManagerFragments()) { + if (isDescendant(fragment.getParentFragment())) { + descendants.add(fragment); + } + } + return Collections.unmodifiableSet(descendants); + } + } + + /** + * Returns true if the fragment is a descendant of our parent. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private boolean isDescendant(Fragment fragment) { + Fragment root = this.getParentFragment(); + while (fragment.getParentFragment() != null) { + if (fragment.getParentFragment() == root) { + return true; + } + fragment = fragment.getParentFragment(); + } + return false; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + rootRequestManagerFragment = RequestManagerRetriever.get() + .getRequestManagerFragment(getActivity().getFragmentManager()); + if (rootRequestManagerFragment != this) { + rootRequestManagerFragment.addChildRequestManagerFragment(this); + } + } + + @Override + public void onDetach() { + super.onDetach(); + if (rootRequestManagerFragment != null) { + rootRequestManagerFragment.removeChildRequestManagerFragment(this); + rootRequestManagerFragment = null; + } + } + + @Override + public void onStart() { + super.onStart(); + lifecycle.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + lifecycle.onStop(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + lifecycle.onDestroy(); + } + + @Override + public void onTrimMemory(int level) { + // If an activity is re-created, onTrimMemory may be called before a manager is ever set. + // See #329. + if (requestManager != null) { + requestManager.onTrimMemory(level); + } + } + + @Override + public void onLowMemory() { + // If an activity is re-created, onLowMemory may be called before a manager is ever set. + // See #329. + if (requestManager != null) { + requestManager.onLowMemory(); + } + } + + private class FragmentRequestManagerTreeNode implements RequestManagerTreeNode { + @Override + public Set getDescendants() { + Set descendantFragments = getDescendantRequestManagerFragments(); + HashSet descendants = + new HashSet(descendantFragments.size()); + for (RequestManagerFragment fragment : descendantFragments) { + if (fragment.getRequestManager() != null) { + descendants.add(fragment.getRequestManager()); + } + } + return descendants; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerRetriever.java b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerRetriever.java new file mode 100755 index 0000000..c2c8200 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerRetriever.java @@ -0,0 +1,229 @@ +package com.example.bumptech.glide.manager; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.util.Log; + + +import com.example.bumptech.glide.RequestManager; +import com.example.bumptech.glide.util.Util; + +import java.util.HashMap; +import java.util.Map; + +/** + * A collection of static methods for creating new {@link RequestManager}s or retrieving existing + * ones from activities and fragment. + */ +public class RequestManagerRetriever implements Handler.Callback { + private static final String TAG = "RMRetriever"; + static final String FRAGMENT_TAG = "com.bumptech.glide.manager"; + + /** The singleton instance of RequestManagerRetriever. */ + private static final RequestManagerRetriever INSTANCE = new RequestManagerRetriever(); + + private static final int ID_REMOVE_FRAGMENT_MANAGER = 1; + private static final int ID_REMOVE_SUPPORT_FRAGMENT_MANAGER = 2; + + /** The top application level RequestManager. */ + private volatile RequestManager applicationManager; + + // Visible for testing. + /** Pending adds for RequestManagerFragments. */ + final Map pendingRequestManagerFragments = + new HashMap(); + + // Visible for testing. + /** Pending adds for SupportRequestManagerFragments. */ + final Map pendingSupportRequestManagerFragments = + new HashMap(); + + /** Main thread handler to handle cleaning up pending fragment maps. */ + private final Handler handler; + + /** + * Retrieves and returns the RequestManagerRetriever singleton. + */ + public static RequestManagerRetriever get() { + return INSTANCE; + } + + // Visible for testing. + RequestManagerRetriever() { + handler = new Handler(Looper.getMainLooper(), this /* Callback */); + } + + private RequestManager getApplicationManager(Context context) { + // Either an application context or we're on a background thread. + if (applicationManager == null) { + synchronized (this) { + if (applicationManager == null) { + // Normally pause/resume is taken care of by the fragment we add to the fragment or activity. + // However, in this case since the manager attached to the application will not receive lifecycle + // events, we must force the manager to start resumed using ApplicationLifecycle. + applicationManager = new RequestManager(context.getApplicationContext(), + new ApplicationLifecycle(), new EmptyRequestManagerTreeNode()); + } + } + } + + return applicationManager; + } + + public RequestManager get(Context context) { + if (context == null) { + throw new IllegalArgumentException("You cannot start a load on a null Context"); + } else if (Util.isOnMainThread() && !(context instanceof Application)) { + if (context instanceof FragmentActivity) { + return get((FragmentActivity) context); + } else if (context instanceof Activity) { + return get((Activity) context); + } else if (context instanceof ContextWrapper) { + return get(((ContextWrapper) context).getBaseContext()); + } + } + + return getApplicationManager(context); + } + + public RequestManager get(FragmentActivity activity) { + if (Util.isOnBackgroundThread()) { + return get(activity.getApplicationContext()); + } else { + assertNotDestroyed(activity); + FragmentManager fm = activity.getSupportFragmentManager(); + return supportFragmentGet(activity, fm); + } + } + + public RequestManager get(Fragment fragment) { + if (fragment.getActivity() == null) { + throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached"); + } + if (Util.isOnBackgroundThread()) { + return get(fragment.getActivity().getApplicationContext()); + } else { + FragmentManager fm = fragment.getChildFragmentManager(); + return supportFragmentGet(fragment.getActivity(), fm); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public RequestManager get(Activity activity) { + if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + return get(activity.getApplicationContext()); + } else { + assertNotDestroyed(activity); + android.app.FragmentManager fm = activity.getFragmentManager(); + return fragmentGet(activity, fm); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private static void assertNotDestroyed(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) { + throw new IllegalArgumentException("You cannot start a load for a destroyed activity"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public RequestManager get(android.app.Fragment fragment) { + if (fragment.getActivity() == null) { + throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached"); + } + if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return get(fragment.getActivity().getApplicationContext()); + } else { + android.app.FragmentManager fm = fragment.getChildFragmentManager(); + return fragmentGet(fragment.getActivity(), fm); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + RequestManagerFragment getRequestManagerFragment(final android.app.FragmentManager fm) { + RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); + if (current == null) { + current = pendingRequestManagerFragments.get(fm); + if (current == null) { + current = new RequestManagerFragment(); + pendingRequestManagerFragments.put(fm, current); + fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); + handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(); + } + } + return current; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + RequestManager fragmentGet(Context context, android.app.FragmentManager fm) { + RequestManagerFragment current = getRequestManagerFragment(fm); + RequestManager requestManager = current.getRequestManager(); + if (requestManager == null) { + requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode()); + current.setRequestManager(requestManager); + } + return requestManager; + } + + SupportRequestManagerFragment getSupportRequestManagerFragment(final FragmentManager fm) { + SupportRequestManagerFragment current = (SupportRequestManagerFragment) fm.findFragmentByTag( + + + FRAGMENT_TAG); + if (current == null) { + current = pendingSupportRequestManagerFragments.get(fm); + if (current == null) { + current = new SupportRequestManagerFragment(); + pendingSupportRequestManagerFragments.put(fm, current); + fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); + handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget(); + } + } + return current; + } + + RequestManager supportFragmentGet(Context context, FragmentManager fm) { + SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm); + RequestManager requestManager = current.getRequestManager(); + if (requestManager == null) { + requestManager = new RequestManager(context, current.getLifeCycle(), current.getRequestManagerTreeNode()); + current.setRequestManager(requestManager); + } + return requestManager; + } + + @Override + public boolean handleMessage(Message message) { + boolean handled = true; + Object removed = null; + Object key = null; + switch (message.what) { + case ID_REMOVE_FRAGMENT_MANAGER: + android.app.FragmentManager fm = (android.app.FragmentManager) message.obj; + key = fm; + removed = pendingRequestManagerFragments.remove(fm); + break; + case ID_REMOVE_SUPPORT_FRAGMENT_MANAGER: + FragmentManager supportFm = (FragmentManager) message.obj; + key = supportFm; + removed = pendingSupportRequestManagerFragments.remove(supportFm); + break; + default: + handled = false; + } + if (handled && removed == null && Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Failed to remove expected request manager fragment, manager: " + key); + } + return handled; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerTreeNode.java b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerTreeNode.java new file mode 100755 index 0000000..f4de36f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/RequestManagerTreeNode.java @@ -0,0 +1,18 @@ +package com.example.bumptech.glide.manager; + + +import com.example.bumptech.glide.RequestManager; + +import java.util.Set; + +/** + * Provides access to the relatives of a RequestManager based on the current context. The context hierarchy + * is provided by nesting in Activity and Fragments; the application context does not provide access to + * any other RequestManagers hierarchically. + */ +public interface RequestManagerTreeNode { + /** + * Returns all descendant {@link RequestManager}s relative to the context of the current {@link RequestManager}. + */ + Set getDescendants(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/RequestTracker.java b/core/src/main/java/com/example/bumptech/glide/manager/RequestTracker.java new file mode 100755 index 0000000..8e61c9c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/RequestTracker.java @@ -0,0 +1,116 @@ +package com.example.bumptech.glide.manager; + + +import com.example.bumptech.glide.request.Request; +import com.example.bumptech.glide.util.Util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * A class for tracking, canceling, and restarting in progress, completed, and failed requests. + */ +public class RequestTracker { + // Most requests will be for views and will therefore be held strongly (and safely) by the view via the tag. + // However, a user can always pass in a different type of target which may end up not being strongly referenced even + // though the user still would like the request to finish. Weak references are therefore only really functional in + // this context for view targets. Despite the side affects, WeakReferences are still essentially required. A user + // can always make repeated requests into targets other than views, or use an activity manager in a fragment pager + // where holding strong references would steadily leak bitmaps and/or views. + private final Set requests = Collections.newSetFromMap(new WeakHashMap()); + // A set of requests that have not completed and are queued to be run again. We use this list to maintain hard + // references to these requests to ensure that they are not garbage collected before they start running or + // while they are paused. See #346. + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final List pendingRequests = new ArrayList(); + + private boolean isPaused; + + /** + * Starts tracking the given request. + */ + public void runRequest(Request request) { + requests.add(request); + if (!isPaused) { + request.begin(); + } else { + pendingRequests.add(request); + } + } + + // Visible for testing. + void addRequest(Request request) { + requests.add(request); + } + + /** + * Stops tracking the given request. + */ + public void removeRequest(Request request) { + requests.remove(request); + pendingRequests.remove(request); + } + + /** + * Returns {@code true} if requests are currently paused, and {@code false} otherwise. + */ + public boolean isPaused() { + return isPaused; + } + + /** + * Stops any in progress requests. + */ + public void pauseRequests() { + isPaused = true; + for (Request request : Util.getSnapshot(requests)) { + if (request.isRunning()) { + request.pause(); + pendingRequests.add(request); + } + } + } + + /** + * Starts any not yet completed or failed requests. + */ + public void resumeRequests() { + isPaused = false; + for (Request request : Util.getSnapshot(requests)) { + if (!request.isComplete() && !request.isCancelled() && !request.isRunning()) { + request.begin(); + } + } + pendingRequests.clear(); + } + + /** + * Cancels all requests and clears their resources. + */ + public void clearRequests() { + for (Request request : Util.getSnapshot(requests)) { + request.clear(); + } + pendingRequests.clear(); + } + + /** + * Restarts failed requests and cancels and restarts in progress requests. + */ + public void restartRequests() { + for (Request request : Util.getSnapshot(requests)) { + if (!request.isComplete() && !request.isCancelled()) { + // Ensure the request will be restarted in onResume. + request.pause(); + if (!isPaused) { + request.begin(); + } else { + pendingRequests.add(request); + } + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/manager/SupportRequestManagerFragment.java b/core/src/main/java/com/example/bumptech/glide/manager/SupportRequestManagerFragment.java new file mode 100755 index 0000000..e850cd4 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/manager/SupportRequestManagerFragment.java @@ -0,0 +1,174 @@ +package com.example.bumptech.glide.manager; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.support.v4.app.Fragment; + + +import com.example.bumptech.glide.RequestManager; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * A view-less {@link Fragment} used to safely store an + * {@link RequestManager} that can be used to start, stop and manage Glide requests started for + * targets within the fragment or activity this fragment is a child of. + * + * @see RequestManagerFragment + * @see RequestManagerRetriever + * @see RequestManager + */ +public class SupportRequestManagerFragment extends Fragment { + private RequestManager requestManager; + private final ActivityFragmentLifecycle lifecycle; + private final RequestManagerTreeNode requestManagerTreeNode = + new SupportFragmentRequestManagerTreeNode(); + private final HashSet childRequestManagerFragments = + new HashSet(); + private SupportRequestManagerFragment rootRequestManagerFragment; + + public SupportRequestManagerFragment() { + this(new ActivityFragmentLifecycle()); + } + + // For testing only. + @SuppressLint("ValidFragment") + public SupportRequestManagerFragment(ActivityFragmentLifecycle lifecycle) { + this.lifecycle = lifecycle; + } + + /** + * Sets the current {@link RequestManager}. + * + * @param requestManager The manager to set. + */ + public void setRequestManager(RequestManager requestManager) { + this.requestManager = requestManager; + } + + public ActivityFragmentLifecycle getLifeCycle() { + return lifecycle; + } + + /** + * Returns the current {@link RequestManager} or null if none is set. + */ + public RequestManager getRequestManager() { + return requestManager; + } + + /** + * Returns the {@link RequestManagerTreeNode} that provides tree traversal methods relative to the associated + * {@link RequestManager}. + */ + public RequestManagerTreeNode getRequestManagerTreeNode() { + return requestManagerTreeNode; + } + + private void addChildRequestManagerFragment(SupportRequestManagerFragment child) { + childRequestManagerFragments.add(child); + } + + private void removeChildRequestManagerFragment(SupportRequestManagerFragment child) { + childRequestManagerFragments.remove(child); + } + + /** + * Returns the set of fragments that this RequestManagerFragment's parent is a parent to. (i.e. our parent is + * the fragment that we are annotating). + */ + public Set getDescendantRequestManagerFragments() { + if (rootRequestManagerFragment == null) { + return Collections.emptySet(); + } else if (rootRequestManagerFragment == this) { + return Collections.unmodifiableSet(childRequestManagerFragments); + } else { + HashSet descendants = + new HashSet(); + for (SupportRequestManagerFragment fragment + : rootRequestManagerFragment.getDescendantRequestManagerFragments()) { + if (isDescendant(fragment.getParentFragment())) { + descendants.add(fragment); + } + } + return Collections.unmodifiableSet(descendants); + } + } + + /** + * Returns true if the fragment is a descendant of our parent. + */ + private boolean isDescendant(Fragment fragment) { + Fragment root = this.getParentFragment(); + while (fragment.getParentFragment() != null) { + if (fragment.getParentFragment() == root) { + return true; + } + fragment = fragment.getParentFragment(); + } + return false; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + rootRequestManagerFragment = RequestManagerRetriever.get() + .getSupportRequestManagerFragment(getActivity().getSupportFragmentManager()); + if (rootRequestManagerFragment != this) { + rootRequestManagerFragment.addChildRequestManagerFragment(this); + } + } + + @Override + public void onDetach() { + super.onDetach(); + if (rootRequestManagerFragment != null) { + rootRequestManagerFragment.removeChildRequestManagerFragment(this); + rootRequestManagerFragment = null; + } + } + + @Override + public void onStart() { + super.onStart(); + lifecycle.onStart(); + } + + @Override + public void onStop() { + super.onStop(); + lifecycle.onStop(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + lifecycle.onDestroy(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + // If an activity is re-created, onLowMemory may be called before a manager is ever set. + // See #329. + if (requestManager != null) { + requestManager.onLowMemory(); + } + } + + private class SupportFragmentRequestManagerTreeNode implements RequestManagerTreeNode { + @Override + public Set getDescendants() { + Set descendantFragments = getDescendantRequestManagerFragments(); + HashSet descendants = new HashSet(descendantFragments.size()); + for (SupportRequestManagerFragment fragment : descendantFragments) { + if (fragment.getRequestManager() != null) { + descendants.add(fragment.getRequestManager()); + } + } + return descendants; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/module/GlideModule.java b/core/src/main/java/com/example/bumptech/glide/module/GlideModule.java new file mode 100755 index 0000000..9f5a509 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/module/GlideModule.java @@ -0,0 +1,98 @@ +package com.example.bumptech.glide.module; + +import android.content.Context; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.GlideBuilder; + + +/** + * An interface allowing lazy configuration of Glide including setting options using + * {@link GlideBuilder} and registering + * {@link com.bumptech.glide.load.model.ModelLoader ModelLoaders}. + * + *

+ * To use this interface: + *

    + *
  1. + * Implement the GlideModule interface in a class with public visibility, calling + * {@link Glide#register(Class, Class, com.bumptech.glide.load.model.ModelLoaderFactory)} + * for each {@link com.bumptech.glide.load.model.ModelLoader} you'd like to register: + *
    + *                  
    + *                      public class FlickrGlideModule implements GlideModule {
    + *                          {@literal @}Override
    + *                          public void applyOptions(Context context, GlideBuilder builder) {
    + *                              buidler.setDecodeFormat(DecodeFormat.ALWAYS_ARGB_8888);
    + *                          }
    + *
    + *                          {@literal @}Override
    + *                          public void registerComponents(Context context, Glide glide) {
    + *                              glide.register(Model.class, Data.class, new MyModelLoader());
    + *                          }
    + *                      }
    + *                  
    + *             
    + *
  2. + *
  3. + * Add your implementation to your list of keeps in your proguard.cfg file: + *
    + *                  {@code
    + *                      -keepnames class * com.bumptech.glide.samples.flickr.FlickrGlideModule
    + *                  }
    + *              
    + *
  4. + *
  5. + * Add a metadata tag to your AndroidManifest.xml with your GlideModule implementation's fully qualified + * classname as the key, and {@code GlideModule} as the value: + *
    + *                 {@code
    + *                      
    + *                 }
    + *             
    + *
  6. + *
+ *

+ * + *

+ * All implementations must be publicly visible and contain only an empty constructor so they can be instantiated + * via reflection when Glide is lazily initialized. + *

+ * + *

+ * There is no defined order in which modules are called, so projects should be careful to avoid applying + * conflicting settings in different modules. If an application depends on libraries that have conflicting + * modules, the application should consider avoiding the library modules and instead providing their required + * dependencies in a single application module. + *

+ */ +public interface GlideModule { + + /** + * Lazily apply options to a {@link GlideBuilder} immediately before the Glide singleton is + * created. + * + *

+ * This method will be called once and only once per implementation. + *

+ * + * @param context An Application {@link Context}. + * @param builder The {@link GlideBuilder} that will be used to create Glide. + */ + void applyOptions(Context context, GlideBuilder builder); + + /** + * Lazily register components immediately after the Glide singleton is created but before any requests can be + * started. + * + *

+ * This method will be called once and only once per implementation. + *

+ * + * @param context An Application {@link Context}. + * @param glide The newly created Glide singleton. + */ + void registerComponents(Context context, Glide glide); +} diff --git a/core/src/main/java/com/example/bumptech/glide/module/ManifestParser.java b/core/src/main/java/com/example/bumptech/glide/module/ManifestParser.java new file mode 100755 index 0000000..97a7019 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/module/ManifestParser.java @@ -0,0 +1,63 @@ +package com.example.bumptech.glide.module; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses {@link GlideModule} references out of the AndroidManifest file. + */ +public final class ManifestParser { + private static final String GLIDE_MODULE_VALUE = "GlideModule"; + + private final Context context; + + public ManifestParser(Context context) { + this.context = context; + } + + public List parse() { + List modules = new ArrayList(); + try { + ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA); + if (appInfo.metaData != null) { + for (String key : appInfo.metaData.keySet()) { + if (GLIDE_MODULE_VALUE.equals(appInfo.metaData.get(key))) { + modules.add(parseModule(key)); + } + } + } + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Unable to find metadata to parse GlideModules", e); + } + + return modules; + } + + private static GlideModule parseModule(String className) { + Class clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unable to find GlideModule implementation", e); + } + + Object module; + try { + module = clazz.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("Unable to instantiate GlideModule implementation for " + clazz, e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate GlideModule implementation for " + clazz, e); + } + + if (!(module instanceof GlideModule)) { + throw new RuntimeException("Expected instanceof GlideModule, but found: " + module); + } + return (GlideModule) module; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/ChildLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/provider/ChildLoadProvider.java new file mode 100755 index 0000000..9d80631 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/ChildLoadProvider.java @@ -0,0 +1,155 @@ +package com.example.bumptech.glide.provider; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +import java.io.File; + +/** + * A {@link LoadProvider} that returns classes preferentially from those set on it but + * that also defaults to a wrapped {@link LoadProvider} when a particular class is not set. + * + * @param The type of the model the resource will be loaded from. + * @param The type of the data that will be retrieved for the model. + * @param The type of the resource that will be decoded from the data. + * @param The type of the resource that will be transcoded from the decoded resource. + */ +public class ChildLoadProvider implements LoadProvider, Cloneable { + private final LoadProvider parent; + + private ResourceDecoder cacheDecoder; + private ResourceDecoder sourceDecoder; + private ResourceEncoder encoder; + private ResourceTranscoder transcoder; + private Encoder sourceEncoder; + + public ChildLoadProvider(LoadProvider parent) { + this.parent = parent; + } + + @Override + public ModelLoader getModelLoader() { + return parent.getModelLoader(); + } + + /** + * Sets the {@link ResourceDecoder} to use for decoding the resource from the disk cache. + * + * @param cacheDecoder The decoder to use. + */ + public void setCacheDecoder(ResourceDecoder cacheDecoder) { + this.cacheDecoder = cacheDecoder; + } + + /** + * Sets the {@link ResourceDecoder} to use to decoding the resource from the original data. + * + * @param sourceDecoder The decoder to use. + */ + public void setSourceDecoder(ResourceDecoder sourceDecoder) { + this.sourceDecoder = sourceDecoder; + } + + /** + * Sets the {@link ResourceEncoder} to use to write the decoded and transformed resource to + * the disk cache. + * + * @param encoder The encoder to use. + */ + public void setEncoder(ResourceEncoder encoder) { + this.encoder = encoder; + } + + /** + * Sets the {@link ResourceTranscoder} to use to transcode the decoded + * resource. + * + * @param transcoder The transcoder to use. + */ + public void setTranscoder(ResourceTranscoder transcoder) { + this.transcoder = transcoder; + } + + /** + * Sets the {@link Encoder} to use to write the original data to the disk cache. + * + * @param sourceEncoder The encoder to use. + */ + public void setSourceEncoder(Encoder sourceEncoder) { + this.sourceEncoder = sourceEncoder; + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceDecoder getCacheDecoder() { + if (cacheDecoder != null) { + return cacheDecoder; + } else { + return parent.getCacheDecoder(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceDecoder getSourceDecoder() { + if (sourceDecoder != null) { + return sourceDecoder; + } else { + return parent.getSourceDecoder(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Encoder getSourceEncoder() { + if (sourceEncoder != null) { + return sourceEncoder; + } else { + return parent.getSourceEncoder(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceEncoder getEncoder() { + if (encoder != null) { + return encoder; + } else { + return parent.getEncoder(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceTranscoder getTranscoder() { + if (transcoder != null) { + return transcoder; + } else { + return parent.getTranscoder(); + } + } + + @SuppressWarnings("unchecked") + @Override + public ChildLoadProvider clone() { + try { + return (ChildLoadProvider) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProvider.java new file mode 100755 index 0000000..d514766 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProvider.java @@ -0,0 +1,39 @@ +package com.example.bumptech.glide.provider; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; + +import java.io.File; + +/** + * A load provider that provides the necessary encoders and decoders to decode a specific type of resource from a + * specific type of data. + * + * @param The type of data the resource will be decoded from. + * @param The type of resource that will be decoded. + */ +public interface DataLoadProvider { + + /** + * Returns the {@link ResourceDecoder} to use to decode the resource from the disk cache. + */ + ResourceDecoder getCacheDecoder(); + + /** + * Returns the {@link ResourceDecoder} to use to decode the resource from the original data. + */ + ResourceDecoder getSourceDecoder(); + + /** + * Returns the {@link Encoder} to use to write the original data to the disk cache. + */ + Encoder getSourceEncoder(); + + /** + * Returns the {@link ResourceEncoder} to use to write the decoded and transformed resource + * to the disk cache. + */ + ResourceEncoder getEncoder(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProviderRegistry.java b/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProviderRegistry.java new file mode 100755 index 0000000..7956d23 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/DataLoadProviderRegistry.java @@ -0,0 +1,54 @@ +package com.example.bumptech.glide.provider; + + +import com.example.bumptech.glide.util.MultiClassKey; + +import java.util.HashMap; +import java.util.Map; + +/** + * A class that allows {@link DataLoadProvider}s to be registered and retrieved by the + * data and resource classes they provide encoders and decoders for. + */ +public class DataLoadProviderRegistry { + private static final MultiClassKey GET_KEY = new MultiClassKey(); + + private final Map> providers = + new HashMap>(); + + /** + * Registers the given {@link DataLoadProvider} using the given classes so it can later + * be retrieved using the given classes. + * + * @param dataClass The class of the data that the provider provides encoders and decoders for. + * @param resourceClass The class of the resource that the provider provides encoders and decoders for. + * @param provider The provider. + * @param The type of the data that the provider provides encoders and decoders for. + * @param The type of the resource that the provider provides encoders and decoders for. + */ + public void register(Class dataClass, Class resourceClass, DataLoadProvider provider) { + //TODO: maybe something like DataLoadProvider may work here + providers.put(new MultiClassKey(dataClass, resourceClass), provider); + } + + /** + * Returns the currently registered {@link DataLoadProvider} for the given classes. + * + * @param dataClass The class of the data that the provider provides encoders and decoders for. + * @param resourceClass The class of the resource that the provider provides encoders and decoders for. + * @param The type of the data that the provider provides encoders and decoders for. + * @param The type of the resource that the provider provides encoders and decoders for. + */ + @SuppressWarnings("unchecked") + public DataLoadProvider get(Class dataClass, Class resourceClass) { + DataLoadProvider result; + synchronized (GET_KEY) { + GET_KEY.set(dataClass, resourceClass); + result = providers.get(GET_KEY); + } + if (result == null) { + result = EmptyDataLoadProvider.get(); + } + return (DataLoadProvider) result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/EmptyDataLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/provider/EmptyDataLoadProvider.java new file mode 100755 index 0000000..c8f057e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/EmptyDataLoadProvider.java @@ -0,0 +1,43 @@ +package com.example.bumptech.glide.provider; + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; + +import java.io.File; + +/** + * A {@link DataLoadProvider} that returns {@code null} for every class. + * + * @param unused data type. + * @param unused resource type. + */ +public class EmptyDataLoadProvider implements DataLoadProvider { + private static final DataLoadProvider EMPTY_DATA_LOAD_PROVIDER = new EmptyDataLoadProvider(); + + @SuppressWarnings("unchecked") + public static DataLoadProvider get() { + return (DataLoadProvider) EMPTY_DATA_LOAD_PROVIDER; + } + + @Override + public ResourceDecoder getCacheDecoder() { + return null; + } + + @Override + public ResourceDecoder getSourceDecoder() { + return null; + } + + @Override + public Encoder getSourceEncoder() { + return null; + } + + @Override + public ResourceEncoder getEncoder() { + return null; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/FixedLoadProvider.java b/core/src/main/java/com/example/bumptech/glide/provider/FixedLoadProvider.java new file mode 100755 index 0000000..32dfffa --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/FixedLoadProvider.java @@ -0,0 +1,92 @@ +package com.example.bumptech.glide.provider; + + + +import com.example.bumptech.glide.load.Encoder; +import com.example.bumptech.glide.load.ResourceDecoder; +import com.example.bumptech.glide.load.ResourceEncoder; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +import java.io.File; + +/** + * A {@link LoadProvider} that sets the classes it provides using non null arguments in its + * constructor. + * + * @param The type of the model the resource will be loaded from. + * @param The type of the data that will be retrieved for the model. + * @param The type of the resource that will be decoded from the data. + * @param The type of the resource that will be transcoded from the decoded resource. + */ +public class FixedLoadProvider implements LoadProvider { + private final ModelLoader modelLoader; + private final ResourceTranscoder transcoder; + private final DataLoadProvider dataLoadProvider; + + public FixedLoadProvider(ModelLoader modelLoader, ResourceTranscoder transcoder, + DataLoadProvider dataLoadProvider) { + if (modelLoader == null) { + throw new NullPointerException("ModelLoader must not be null"); + } + this.modelLoader = modelLoader; + + if (transcoder == null) { + throw new NullPointerException("Transcoder must not be null"); + } + this.transcoder = transcoder; + + if (dataLoadProvider == null) { + throw new NullPointerException("DataLoadProvider must not be null"); + } + this.dataLoadProvider = dataLoadProvider; + } + + /** + * {@inheritDoc} + */ + @Override + public ModelLoader getModelLoader() { + return modelLoader; + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceTranscoder getTranscoder() { + return transcoder; + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceDecoder getCacheDecoder() { + return dataLoadProvider.getCacheDecoder(); + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceDecoder getSourceDecoder() { + return dataLoadProvider.getSourceDecoder(); + } + + /** + * {@inheritDoc} + */ + @Override + public Encoder getSourceEncoder() { + return dataLoadProvider.getSourceEncoder(); + } + + /** + * {@inheritDoc} + */ + @Override + public ResourceEncoder getEncoder() { + return dataLoadProvider.getEncoder(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/provider/LoadProvider.java b/core/src/main/java/com/example/bumptech/glide/provider/LoadProvider.java new file mode 100755 index 0000000..b81dded --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/provider/LoadProvider.java @@ -0,0 +1,29 @@ +package com.example.bumptech.glide.provider; + + +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +/** + * An extension of {@link DataLoadProvider} that also allows a + * {@link ModelLoader} and a + * {@link ResourceTranscoder} to be retrieved. + * + * @param The type of model. + * @param The type of data that will be decoded from. + * @param The type of resource that will be decoded. + * @param The type of resource that the decoded resource will be transcoded to. + */ +public interface LoadProvider extends DataLoadProvider { + + /** + * Returns the {@link ModelLoader} to convert from the given model to a data type. + */ + ModelLoader getModelLoader(); + + /** + * Returns the {@link ResourceTranscoder} to convert from the decoded + * and transformed resource into the transcoded resource. + */ + ResourceTranscoder getTranscoder(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/FutureTarget.java b/core/src/main/java/com/example/bumptech/glide/request/FutureTarget.java new file mode 100755 index 0000000..00c90e9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/FutureTarget.java @@ -0,0 +1,36 @@ +package com.example.bumptech.glide.request; + + +import com.example.bumptech.glide.request.target.Target; + +import java.util.concurrent.Future; + +/** + * An interface for an object that is both a {@link Target} and a + * {@link Future}. For example: + *
+ * {@code
+ * FutureTarget futureTarget = Glide.with(fragment)
+ *                                       .load("http://goo.gl/1asf12")
+ *                                       .asBitmap()
+ *                                       .into(250, 250);
+ * Bitmap myBitmap = futureTarget.get();
+ * ... // do things with bitmap and then release when finished:
+ * Glide.clear(futureTarget);
+ * }
+ * 
+ * + *

+ * Note - {@link #get()} and {@link #get(long, java.util.concurrent.TimeUnit)} must be called + * off of the main thread or they will block forever. + *

+ * + * @param The type of resource this FutureTarget will retrieve. + */ +public interface FutureTarget extends Future, Target { + + /** + * Safely clears the target from a background thread to release its resources. + */ + void clear(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/GenericRequest.java b/core/src/main/java/com/example/bumptech/glide/request/GenericRequest.java new file mode 100755 index 0000000..89766b9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/GenericRequest.java @@ -0,0 +1,556 @@ +package com.example.bumptech.glide.request; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Log; + + +import com.example.bumptech.glide.Priority; +import com.example.bumptech.glide.load.Key; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.data.DataFetcher; +import com.example.bumptech.glide.load.engine.DiskCacheStrategy; +import com.example.bumptech.glide.load.engine.Engine; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.model.ModelLoader; +import com.example.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.example.bumptech.glide.provider.LoadProvider; +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.animation.GlideAnimationFactory; +import com.example.bumptech.glide.request.target.SizeReadyCallback; +import com.example.bumptech.glide.request.target.Target; +import com.example.bumptech.glide.util.LogTime; +import com.example.bumptech.glide.util.Util; + +import java.util.Queue; + +/** + * A {@link Request} that loads a {@link Resource} into a given {@link Target}. + * + * @param
The type of the model that the resource will be loaded from. + * @param The type of the data that the resource will be loaded from. + * @param The type of the resource that will be loaded. + * @param The type of the resource that will be transcoded from the loaded resource. + */ +public final class GenericRequest implements Request, SizeReadyCallback, + ResourceCallback { + private static final String TAG = "GenericRequest"; + private static final Queue> REQUEST_POOL = Util.createQueue(0); + private static final double TO_MEGABYTE = 1d / (1024d * 1024d); + + private enum Status { + /** Created but not yet running. */ + PENDING, + /** In the process of fetching media. */ + RUNNING, + /** Waiting for a callback given to the Target to be called to determine target dimensions. */ + WAITING_FOR_SIZE, + /** Finished loading media successfully. */ + COMPLETE, + /** Failed to load media, may be restarted. */ + FAILED, + /** Cancelled by the user, may not be restarted. */ + CANCELLED, + /** Cleared by the user with a placeholder set, may not be restarted. */ + CLEARED, + /** Temporarily paused by the system, may be restarted. */ + PAUSED, + } + + private final String tag = String.valueOf(hashCode()); + + private Key signature; + private Drawable fallbackDrawable; + private int fallbackResourceId; + private int placeholderResourceId; + private int errorResourceId; + private Context context; + private Transformation transformation; + private LoadProvider loadProvider; + private RequestCoordinator requestCoordinator; + private A model; + private Class transcodeClass; + private boolean isMemoryCacheable; + private Priority priority; + private Target target; + private RequestListener requestListener; + private float sizeMultiplier; + private Engine engine; + private GlideAnimationFactory animationFactory; + private int overrideWidth; + private int overrideHeight; + private DiskCacheStrategy diskCacheStrategy; + + private Drawable placeholderDrawable; + private Drawable errorDrawable; + private boolean loadedFromMemoryCache; + // doing our own type check + private Resource resource; + private Engine.LoadStatus loadStatus; + private long startTime; + private Status status; + + public static GenericRequest obtain( + LoadProvider loadProvider, + A model, + Key signature, + Context context, + Priority priority, + Target target, + float sizeMultiplier, + Drawable placeholderDrawable, + int placeholderResourceId, + Drawable errorDrawable, + int errorResourceId, + Drawable fallbackDrawable, + int fallbackResourceId, + RequestListener requestListener, + RequestCoordinator requestCoordinator, + Engine engine, + Transformation transformation, + Class transcodeClass, + boolean isMemoryCacheable, + GlideAnimationFactory animationFactory, + int overrideWidth, + int overrideHeight, + DiskCacheStrategy diskCacheStrategy) { + @SuppressWarnings("unchecked") + GenericRequest request = (GenericRequest) REQUEST_POOL.poll(); + if (request == null) { + request = new GenericRequest(); + } + request.init(loadProvider, + model, + signature, + context, + priority, + target, + sizeMultiplier, + placeholderDrawable, + placeholderResourceId, + errorDrawable, + errorResourceId, + fallbackDrawable, + fallbackResourceId, + requestListener, + requestCoordinator, + engine, + transformation, + transcodeClass, + isMemoryCacheable, + animationFactory, + overrideWidth, + overrideHeight, + diskCacheStrategy); + return request; + } + + private GenericRequest() { + // just create, instances are reused with recycle/init + } + + @Override + public void recycle() { + loadProvider = null; + model = null; + context = null; + target = null; + placeholderDrawable = null; + errorDrawable = null; + fallbackDrawable = null; + requestListener = null; + requestCoordinator = null; + transformation = null; + animationFactory = null; + loadedFromMemoryCache = false; + loadStatus = null; + REQUEST_POOL.offer(this); + } + + private void init( + LoadProvider loadProvider, + A model, + Key signature, + Context context, + Priority priority, + Target target, + float sizeMultiplier, + Drawable placeholderDrawable, + int placeholderResourceId, + Drawable errorDrawable, + int errorResourceId, + Drawable fallbackDrawable, + int fallbackResourceId, + RequestListener requestListener, + RequestCoordinator requestCoordinator, + Engine engine, + Transformation transformation, + Class transcodeClass, + boolean isMemoryCacheable, + GlideAnimationFactory animationFactory, + int overrideWidth, + int overrideHeight, + DiskCacheStrategy diskCacheStrategy) { + this.loadProvider = loadProvider; + this.model = model; + this.signature = signature; + this.fallbackDrawable = fallbackDrawable; + this.fallbackResourceId = fallbackResourceId; + this.context = context.getApplicationContext(); + this.priority = priority; + this.target = target; + this.sizeMultiplier = sizeMultiplier; + this.placeholderDrawable = placeholderDrawable; + this.placeholderResourceId = placeholderResourceId; + this.errorDrawable = errorDrawable; + this.errorResourceId = errorResourceId; + this.requestListener = requestListener; + this.requestCoordinator = requestCoordinator; + this.engine = engine; + this.transformation = transformation; + this.transcodeClass = transcodeClass; + this.isMemoryCacheable = isMemoryCacheable; + this.animationFactory = animationFactory; + this.overrideWidth = overrideWidth; + this.overrideHeight = overrideHeight; + this.diskCacheStrategy = diskCacheStrategy; + status = Status.PENDING; + + // We allow null models by just setting an error drawable. Null models will always have empty providers, we + // simply skip our sanity checks in that unusual case. + if (model != null) { + check("ModelLoader", loadProvider.getModelLoader(), "try .using(ModelLoader)"); + check("Transcoder", loadProvider.getTranscoder(), "try .as*(Class).transcode(ResourceTranscoder)"); + check("Transformation", transformation, "try .transform(UnitTransformation.get())"); + if (diskCacheStrategy.cacheSource()) { + check("SourceEncoder", loadProvider.getSourceEncoder(), + "try .sourceEncoder(Encoder) or .diskCacheStrategy(NONE/RESULT)"); + } else { + check("SourceDecoder", loadProvider.getSourceDecoder(), + "try .decoder/.imageDecoder/.videoDecoder(ResourceDecoder) or .diskCacheStrategy(ALL/SOURCE)"); + } + if (diskCacheStrategy.cacheSource() || diskCacheStrategy.cacheResult()) { + // TODO if(resourceClass.isAssignableFrom(InputStream.class) it is possible to wrap sourceDecoder + // and use it instead of cacheDecoder: new FileToStreamDecoder(sourceDecoder) + // in that case this shouldn't throw + check("CacheDecoder", loadProvider.getCacheDecoder(), + "try .cacheDecoder(ResouceDecoder) or .diskCacheStrategy(NONE)"); + } + if (diskCacheStrategy.cacheResult()) { + check("Encoder", loadProvider.getEncoder(), + "try .encode(ResourceEncoder) or .diskCacheStrategy(NONE/SOURCE)"); + } + } + } + + private static void check(String name, Object object, String suggestion) { + if (object == null) { + StringBuilder message = new StringBuilder(name); + message.append(" must not be null"); + if (suggestion != null) { + message.append(", "); + message.append(suggestion); + } + throw new NullPointerException(message.toString()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void begin() { + startTime = LogTime.getLogTime(); + if (model == null) { + onException(null); + return; + } + + status = Status.WAITING_FOR_SIZE; + if (Util.isValidDimensions(overrideWidth, overrideHeight)) { + onSizeReady(overrideWidth, overrideHeight); + } else { + target.getSize(this); + } + + if (!isComplete() && !isFailed() && canNotifyStatusChanged()) { + target.onLoadStarted(getPlaceholderDrawable()); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logV("finished run method in " + LogTime.getElapsedMillis(startTime)); + } + } + + /** + * Cancels the current load but does not release any resources held by the request and continues to display + * the loaded resource if the load completed before the call to cancel. + * + *

+ * Cancelled requests can be restarted with a subsequent call to {@link #begin()}. + *

+ * + * @see #clear() + */ + void cancel() { + status = Status.CANCELLED; + if (loadStatus != null) { + loadStatus.cancel(); + loadStatus = null; + } + } + + /** + * Cancels the current load if it is in progress, clears any resources held onto by the request and replaces + * the loaded resource if the load completed with the placeholder. + * + *

+ * Cleared requests can be restarted with a subsequent call to {@link #begin()} + *

+ * + * @see #cancel() + */ + @Override + public void clear() { + Util.assertMainThread(); + if (status == Status.CLEARED) { + return; + } + cancel(); + // Resource must be released before canNotifyStatusChanged is called. + if (resource != null) { + releaseResource(resource); + } + if (canNotifyStatusChanged()) { + target.onLoadCleared(getPlaceholderDrawable()); + } + // Must be after cancel(). + status = Status.CLEARED; + } + + @Override + public boolean isPaused() { + return status == Status.PAUSED; + } + + @Override + public void pause() { + clear(); + status = Status.PAUSED; + } + + private void releaseResource(Resource resource) { + engine.release(resource); + this.resource = null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRunning() { + return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isComplete() { + return status == Status.COMPLETE; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isResourceSet() { + return isComplete(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCancelled() { + return status == Status.CANCELLED || status == Status.CLEARED; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isFailed() { + return status == Status.FAILED; + } + + private Drawable getFallbackDrawable() { + if (fallbackDrawable == null && fallbackResourceId > 0) { + fallbackDrawable = context.getResources().getDrawable(fallbackResourceId); + } + return fallbackDrawable; + } + + private void setErrorPlaceholder(Exception e) { + if (!canNotifyStatusChanged()) { + return; + } + + Drawable error = model == null ? getFallbackDrawable() : null; + if (error == null) { + error = getErrorDrawable(); + } + if (error == null) { + error = getPlaceholderDrawable(); + } + target.onLoadFailed(e, error); + } + + private Drawable getErrorDrawable() { + if (errorDrawable == null && errorResourceId > 0) { + errorDrawable = context.getResources().getDrawable(errorResourceId); + } + return errorDrawable; + } + + private Drawable getPlaceholderDrawable() { + if (placeholderDrawable == null && placeholderResourceId > 0) { + placeholderDrawable = context.getResources().getDrawable(placeholderResourceId); + } + return placeholderDrawable; + } + + /** + * A callback method that should never be invoked directly. + */ + @Override + public void onSizeReady(int width, int height) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime)); + } + if (status != Status.WAITING_FOR_SIZE) { + return; + } + status = Status.RUNNING; + + width = Math.round(sizeMultiplier * width); + height = Math.round(sizeMultiplier * height); + + ModelLoader modelLoader = loadProvider.getModelLoader(); + final DataFetcher dataFetcher = modelLoader.getResourceFetcher(model, width, height); + + if (dataFetcher == null) { + onException(new Exception("Failed to load model: \'" + model + "\'")); + return; + } + ResourceTranscoder transcoder = loadProvider.getTranscoder(); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime)); + } + loadedFromMemoryCache = true; + loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder, + priority, isMemoryCacheable, diskCacheStrategy, this); + loadedFromMemoryCache = resource != null; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); + } + } + + private boolean canSetResource() { + return requestCoordinator == null || requestCoordinator.canSetImage(this); + } + + private boolean canNotifyStatusChanged() { + return requestCoordinator == null || requestCoordinator.canNotifyStatusChanged(this); + } + + private boolean isFirstReadyResource() { + return requestCoordinator == null || !requestCoordinator.isAnyResourceSet(); + } + + private void notifyLoadSuccess() { + if (requestCoordinator != null) { + requestCoordinator.onRequestSuccess(this); + } + } + + /** + * A callback method that should never be invoked directly. + */ + @SuppressWarnings("unchecked") + @Override + public void onResourceReady(Resource resource) { + if (resource == null) { + onException(new Exception("Expected to receive a Resource with an object of " + transcodeClass + + " inside, but instead got null.")); + return; + } + + Object received = resource.get(); + if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) { + releaseResource(resource); + onException(new Exception("Expected to receive an object of " + transcodeClass + + " but instead got " + (received != null ? received.getClass() : "") + "{" + received + "}" + + " inside Resource{" + resource + "}." + + (received != null ? "" : " " + + "To indicate failure return a null Resource object, " + + "rather than a Resource object containing null data.") + )); + return; + } + + if (!canSetResource()) { + releaseResource(resource); + // We can't set the status to complete before asking canSetResource(). + status = Status.COMPLETE; + return; + } + + onResourceReady(resource, (R) received); + } + + /** + * Internal {@link #onResourceReady(Resource)} where arguments are known to be safe. + * + * @param resource original {@link Resource}, never null + * @param result object returned by {@link Resource#get()}, checked for type and never null + */ + private void onResourceReady(Resource resource, R result) { + // We must call isFirstReadyResource before setting status. + boolean isFirstResource = isFirstReadyResource(); + status = Status.COMPLETE; + this.resource = resource; + + if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache, + isFirstResource)) { + GlideAnimation animation = animationFactory.build(loadedFromMemoryCache, isFirstResource); + target.onResourceReady(result, animation); + } + + notifyLoadSuccess(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + logV("Resource ready in " + LogTime.getElapsedMillis(startTime) + " size: " + + (resource.getSize() * TO_MEGABYTE) + " fromCache: " + loadedFromMemoryCache); + } + } + + /** + * A callback method that should never be invoked directly. + */ + @Override + public void onException(Exception e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "load failed", e); + } + + status = Status.FAILED; + //TODO: what if this is a thumbnail request? + if (requestListener == null || !requestListener.onException(e, model, target, isFirstReadyResource())) { + setErrorPlaceholder(e); + } + } + + private void logV(String message) { + Log.v(TAG, message + " this: " + tag); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/Request.java b/core/src/main/java/com/example/bumptech/glide/request/Request.java new file mode 100755 index 0000000..6ad2a51 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/Request.java @@ -0,0 +1,59 @@ +package com.example.bumptech.glide.request; + +/** + * A request that loads a resource for an {@link com.bumptech.glide.request.target.Target}. + */ +public interface Request { + + /** + * Starts an asynchronous load. + */ + void begin(); + + /** + * Identical to {@link #clear()} except that the request may later be restarted. + */ + void pause(); + + /** + * Prevents any bitmaps being loaded from previous requests, releases any resources held by this request, + * displays the current placeholder if one was provided, and marks the request as having been cancelled. + */ + void clear(); + + /** + * Returns true if this request is paused and may be restarted. + */ + boolean isPaused(); + + /** + * Returns true if this request is running and has not completed or failed. + */ + boolean isRunning(); + + /** + * Returns true if the request has completed successfully. + */ + boolean isComplete(); + + /** + * Returns true if a non-placeholder resource is set. For Requests that load more than one resource, isResourceSet + * may return true even if {@link #isComplete()}} returns false. + */ + boolean isResourceSet(); + + /** + * Returns true if the request has been cancelled. + */ + boolean isCancelled(); + + /** + * Returns true if the request has failed. + */ + boolean isFailed(); + + /** + * Recycles the request object and releases its resources. + */ + void recycle(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/RequestCoordinator.java b/core/src/main/java/com/example/bumptech/glide/request/RequestCoordinator.java new file mode 100755 index 0000000..a960735 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/RequestCoordinator.java @@ -0,0 +1,33 @@ +package com.example.bumptech.glide.request; + +/** + * An interface for coordinating multiple requests with the same {@link com.bumptech.glide.request.target.Target}. + */ +public interface RequestCoordinator { + + /** + * Returns true if the {@link Request} can display a loaded bitmap. + * + * @param request The {@link Request} requesting permission to display a bitmap. + */ + boolean canSetImage(Request request); + + /** + * Returns true if the {@link Request} can display a placeholder. + * + * @param request The {@link Request} requesting permission to display a placeholder. + */ + boolean canNotifyStatusChanged(Request request); + + /** + * Returns true if any coordinated {@link Request} has successfully completed. + * + * @see Request#isComplete() + */ + boolean isAnyResourceSet(); + + /** + * Must be called when a request coordinated by this object completes successfully. + */ + void onRequestSuccess(Request request); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/RequestFutureTarget.java b/core/src/main/java/com/example/bumptech/glide/request/RequestFutureTarget.java new file mode 100755 index 0000000..1c8693b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/RequestFutureTarget.java @@ -0,0 +1,246 @@ +package com.example.bumptech.glide.request; + +import android.graphics.drawable.Drawable; +import android.os.Handler; + + +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.target.SizeReadyCallback; +import com.example.bumptech.glide.util.Util; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * A {@link java.util.concurrent.Future} implementation for Glide that can be used to load resources in a blocking + * manner on background threads. + * + *

+ * Note - Unlike most targets, RequestFutureTargets can be used once and only once. Attempting to reuse a + * RequestFutureTarget will probably result in undesirable behavior or exceptions. Instead of reusing + * objects of this class, the pattern should be: + * + *

+ *     {@code
+ *      RequestFutureTarget target = Glide.load("")...
+ *     Object resource = target.get();
+ *     // Do something with resource, and when finished:
+ *     Glide.clear(target);
+ *     }
+ *     
+ * The {@link com.bumptech.glide.Glide#clear(FutureTarget)} call will make sure any resources used are recycled. + *

+ * + * @param The type of the data to load. + * @param The type of the resource that will be loaded. + */ +public class RequestFutureTarget implements FutureTarget, Runnable { + private static final Waiter DEFAULT_WAITER = new Waiter(); + + private final Handler mainHandler; + private final int width; + private final int height; + // Exists for testing only. + private final boolean assertBackgroundThread; + private final Waiter waiter; + + private R resource; + private Request request; + private boolean isCancelled; + private Exception exception; + private boolean resultReceived; + private boolean exceptionReceived; + + /** + * Constructor for a RequestFutureTarget. Should not be used directly. + */ + public RequestFutureTarget(Handler mainHandler, int width, int height) { + this(mainHandler, width, height, true, DEFAULT_WAITER); + } + + RequestFutureTarget(Handler mainHandler, int width, int height, boolean assertBackgroundThread, Waiter waiter) { + this.mainHandler = mainHandler; + this.width = width; + this.height = height; + this.assertBackgroundThread = assertBackgroundThread; + this.waiter = waiter; + } + + @Override + public synchronized boolean cancel(boolean mayInterruptIfRunning) { + if (isCancelled) { + return true; + } + + final boolean result = !isDone(); + if (result) { + isCancelled = true; + if (mayInterruptIfRunning) { + clear(); + } + waiter.notifyAll(this); + } + return result; + } + + @Override + public synchronized boolean isCancelled() { + return isCancelled; + } + + @Override + public synchronized boolean isDone() { + return isCancelled || resultReceived; + } + + @Override + public R get() throws InterruptedException, ExecutionException { + try { + return doGet(null); + } catch (TimeoutException e) { + throw new AssertionError(e); + } + } + + @Override + public R get(long time, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException { + return doGet(timeUnit.toMillis(time)); + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public void getSize(SizeReadyCallback cb) { + cb.onSizeReady(width, height); + } + + @Override + public void setRequest(Request request) { + this.request = request; + } + + @Override + public Request getRequest() { + return request; + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public void onLoadCleared(Drawable placeholder) { + // Do nothing. + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public void onLoadStarted(Drawable placeholder) { + // Do nothing. + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public synchronized void onLoadFailed(Exception e, Drawable errorDrawable) { + // We might get a null exception. + exceptionReceived = true; + this.exception = e; + waiter.notifyAll(this); + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public synchronized void onResourceReady(R resource, GlideAnimation glideAnimation) { + // We might get a null result. + resultReceived = true; + this.resource = resource; + waiter.notifyAll(this); + } + + private synchronized R doGet(Long timeoutMillis) throws ExecutionException, InterruptedException, TimeoutException { + if (assertBackgroundThread) { + Util.assertBackgroundThread(); + } + + if (isCancelled) { + throw new CancellationException(); + } else if (exceptionReceived) { + throw new ExecutionException(exception); + } else if (resultReceived) { + return resource; + } + + if (timeoutMillis == null) { + waiter.waitForTimeout(this, 0); + } else if (timeoutMillis > 0) { + waiter.waitForTimeout(this, timeoutMillis); + } + + if (Thread.interrupted()) { + throw new InterruptedException(); + } else if (exceptionReceived) { + throw new ExecutionException(exception); + } else if (isCancelled) { + throw new CancellationException(); + } else if (!resultReceived) { + throw new TimeoutException(); + } + + return resource; + } + + /** + * A callback that should never be invoked directly. + */ + @Override + public void run() { + if (request != null) { + request.clear(); + cancel(false /*mayInterruptIfRunning*/); + } + } + + /** + * Can be safely called from either the main thread or a background thread to cleanup the resources used by this + * target. + */ + @Override + public void clear() { + mainHandler.post(this); + } + + @Override + public void onStart() { + // Do nothing. + } + + @Override + public void onStop() { + // Do nothing. + } + + @Override + public void onDestroy() { + // Do nothing. + } + + // Visible for testing. + static class Waiter { + + public void waitForTimeout(Object toWaitOn, long timeoutMillis) throws InterruptedException { + toWaitOn.wait(timeoutMillis); + } + + public void notifyAll(Object toNotify) { + toNotify.notifyAll(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/RequestListener.java b/core/src/main/java/com/example/bumptech/glide/request/RequestListener.java new file mode 100755 index 0000000..653f305 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/RequestListener.java @@ -0,0 +1,62 @@ +package com.example.bumptech.glide.request; + + +import com.example.bumptech.glide.request.target.Target; + +/** + * A class for monitoring the status of a request while images load. + * + * @param The type of the model being loaded. + * @param The type of resource being loaded. + */ +public interface RequestListener { + + /** + * Called when an exception occurs during a load. Will only be called if we currently want to display an image + * for the given model in the given target. It is recommended to create a single instance per activity/fragment + * rather than instantiate a new object for each call to {@code Glide.load()} to avoid object churn. + * + *

+ * It is safe to reload this or a different model or change what is displayed in the target at this point. + * For example: + *

+     * {@code
+     * public void onException(Exception e, T model, Target target, boolean isFirstResource) {
+     *     target.setPlaceholder(R.drawable.a_specific_error_for_my_exception);
+     *     Glide.load(model).into(target);
+     * }
+     * }
+     * 
+ *

+ * + *

+ * Note - if you want to reload this or any other model after an exception, you will need to include all + * relevant builder calls (like centerCrop, placeholder etc). + *

+ * + * @param e The exception, or null. + * @param model The model we were trying to load when the exception occurred. + * @param target The {@link Target} we were trying to load the image into. + * @param isFirstResource True if this exception is for the first resource to load. + * @return True if the listener has handled updating the target for the given exception, false to allow + * Glide's request to update the target. + */ + boolean onException(Exception e, T model, Target target, boolean isFirstResource); + + /** + * Called when a load completes successfully, immediately after + * {@link Target#onResourceReady(Object, com.bumptech.glide.request.animation.GlideAnimation)}. + * + * @param resource The resource that was loaded for the target. + * @param model The specific model that was used to load the image. + * @param target The target the model was loaded into. + * @param isFromMemoryCache True if the load completed synchronously (useful for determining whether or not to + * animate) + * @param isFirstResource True if this is the first resource to in this load to be loaded into the target. For + * example when loading a thumbnail and a fullsize image, this will be true for the first + * image to load and false for the second. + * @return True if the listener has handled setting the resource on the target (including any animations), false to + * allow Glide's request to update the target (again including animations). + */ + boolean onResourceReady(R resource, T model, Target target, boolean isFromMemoryCache, boolean isFirstResource); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/ResourceCallback.java b/core/src/main/java/com/example/bumptech/glide/request/ResourceCallback.java new file mode 100755 index 0000000..1cb146f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/ResourceCallback.java @@ -0,0 +1,25 @@ +package com.example.bumptech.glide.request; + + +import com.example.bumptech.glide.load.engine.Resource; + +/** + * A callback that listens for when a resource load completes successfully or fails due to an exception. + */ +public interface ResourceCallback { + + /** + * Called when a resource is successfully loaded. + * + * @param resource The loaded resource. + */ + void onResourceReady(Resource resource); + + /** + * Called when a resource fails to load successfully. + * + * @param e The exception that caused the failure, or null it the load failed for some reason other than an + * exception. + */ + void onException(Exception e); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/ThumbnailRequestCoordinator.java b/core/src/main/java/com/example/bumptech/glide/request/ThumbnailRequestCoordinator.java new file mode 100755 index 0000000..4003f49 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/ThumbnailRequestCoordinator.java @@ -0,0 +1,156 @@ +package com.example.bumptech.glide.request; + +/** + * A coordinator that coordinates two individual {@link Request}s that load a small thumbnail version of an image and + * the full size version of the image at the same time. + */ +public class ThumbnailRequestCoordinator implements RequestCoordinator, Request { + private Request full; + private Request thumb; + private RequestCoordinator coordinator; + + public ThumbnailRequestCoordinator() { + this(null); + } + + public ThumbnailRequestCoordinator(RequestCoordinator coordinator) { + this.coordinator = coordinator; + } + + public void setRequests(Request full, Request thumb) { + this.full = full; + this.thumb = thumb; + } + + /** + * + * Returns true if the request is either the request loading the fullsize image or if the request loading the + * full size image has not yet completed. + * + * @param request {@inheritDoc} + */ + @Override + public boolean canSetImage(Request request) { + return parentCanSetImage() && (request.equals(full) || !full.isResourceSet()); + } + + private boolean parentCanSetImage() { + return coordinator == null || coordinator.canSetImage(this); + } + + /** + * Returns true if the request is the request loading the fullsize image and if neither the full nor the thumbnail + * image have completed sucessfully. + * + * @param request {@inheritDoc}. + */ + @Override + public boolean canNotifyStatusChanged(Request request) { + return parentCanNotifyStatusChanged() && request.equals(full) && !isAnyResourceSet(); + } + + private boolean parentCanNotifyStatusChanged() { + return coordinator == null || coordinator.canNotifyStatusChanged(this); + } + + @Override + public boolean isAnyResourceSet() { + return parentIsAnyResourceSet() || isResourceSet(); + } + + @Override + public void onRequestSuccess(Request request) { + if (request.equals(thumb)) { + return; + } + if (coordinator != null) { + coordinator.onRequestSuccess(this); + } + // Clearing the thumb is not necessarily safe if the thumb is being displayed in the Target, + // as a layer in a cross fade for example. The only way we know the thumb is not being + // displayed and is therefore safe to clear is if the thumb request has not yet completed. + if (!thumb.isComplete()) { + thumb.clear(); + } + } + + private boolean parentIsAnyResourceSet() { + return coordinator != null && coordinator.isAnyResourceSet(); + } + + /** + * Starts first the thumb request and then the full request. + */ + @Override + public void begin() { + if (!thumb.isRunning()) { + thumb.begin(); + } + if (!full.isRunning()) { + full.begin(); + } + } + + @Override + public void pause() { + full.pause(); + thumb.pause(); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + thumb.clear(); + full.clear(); + } + + @Override + public boolean isPaused() { + return full.isPaused(); + } + + /** + * Returns true if the full request is still running. + */ + @Override + public boolean isRunning() { + return full.isRunning(); + } + + /** + * Returns true if the full request is complete. + */ + @Override + public boolean isComplete() { + return full.isComplete() || thumb.isComplete(); + } + + @Override + public boolean isResourceSet() { + return full.isResourceSet() || thumb.isResourceSet(); + } + + @Override + public boolean isCancelled() { + return full.isCancelled(); + } + + /** + * Returns true if the full request has failed. + */ + @Override + public boolean isFailed() { + return full.isFailed(); + } + + /** + * {@inheritDoc}. + */ + @Override + public void recycle() { + full.recycle(); + thumb.recycle(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeFactory.java b/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeFactory.java new file mode 100755 index 0000000..924fc03 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeFactory.java @@ -0,0 +1,94 @@ +package com.example.bumptech.glide.request.animation; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; + +/** + * A factory class that produces a new {@link GlideAnimation} that varies depending + * on whether or not the drawable was loaded from the memory cache and whether or not the drawable is the first + * image to be set on the target. + * + *

+ * Resources are usually loaded from the memory cache just before the user can see the view, + * for example when the user changes screens or scrolls back and forth in a list. In those cases the user + * typically does not expect to see an animation. As a result, when the resource is loaded from the memory + * cache this factory produces an {@link NoAnimation}. + *

+ * + * @param The type of the {@link Drawable} that will be animated. + */ +public class DrawableCrossFadeFactory implements GlideAnimationFactory { + private static final int DEFAULT_DURATION_MS = 300; + private final ViewAnimationFactory animationFactory; + private final int duration; + private DrawableCrossFadeViewAnimation firstResourceAnimation; + private DrawableCrossFadeViewAnimation secondResourceAnimation; + + public DrawableCrossFadeFactory() { + this(DEFAULT_DURATION_MS); + } + + public DrawableCrossFadeFactory(int duration) { + this(new ViewAnimationFactory(new DefaultAnimationFactory(duration)), duration); + } + + public DrawableCrossFadeFactory(Context context, int defaultAnimationId, int duration) { + this(new ViewAnimationFactory(context, defaultAnimationId), duration); + } + + public DrawableCrossFadeFactory(Animation defaultAnimation, int duration) { + this(new ViewAnimationFactory(defaultAnimation), duration); + } + + DrawableCrossFadeFactory(ViewAnimationFactory animationFactory, int duration) { + this.animationFactory = animationFactory; + this.duration = duration; + } + + @Override + public GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource) { + if (isFromMemoryCache) { + return NoAnimation.get(); + } else if (isFirstResource) { + return getFirstResourceAnimation(); + } else { + return getSecondResourceAnimation(); + } + } + + private GlideAnimation getFirstResourceAnimation() { + if (firstResourceAnimation == null) { + GlideAnimation defaultAnimation = animationFactory.build(false /*isFromMemoryCache*/, + true /*isFirstResource*/); + firstResourceAnimation = new DrawableCrossFadeViewAnimation(defaultAnimation, duration); + } + return firstResourceAnimation; + } + + private GlideAnimation getSecondResourceAnimation() { + if (secondResourceAnimation == null) { + GlideAnimation defaultAnimation = animationFactory.build(false /*isFromMemoryCache*/, + false /*isFirstResource*/); + secondResourceAnimation = new DrawableCrossFadeViewAnimation(defaultAnimation, duration); + } + return secondResourceAnimation; + } + + private static class DefaultAnimationFactory implements ViewAnimation.AnimationFactory { + + private final int duration; + + DefaultAnimationFactory(int duration) { + this.duration = duration; + } + + @Override + public Animation build() { + AlphaAnimation animation = new AlphaAnimation(0f, 1f); + animation.setDuration(duration); + return animation; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeViewAnimation.java b/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeViewAnimation.java new file mode 100755 index 0000000..6ba9b81 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/DrawableCrossFadeViewAnimation.java @@ -0,0 +1,56 @@ +package com.example.bumptech.glide.request.animation; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; + +/** + * A cross fade {@link GlideAnimation} for {@link Drawable}s + * that uses an {@link TransitionDrawable} to transition from an existing drawable + * already visible on the target to a new drawable. If no existing drawable exists, this class can instead fall back + * to a default animation that doesn't rely on {@link TransitionDrawable}. + * + * @param The type of the {@link Drawable} that will be animated. + */ +public class DrawableCrossFadeViewAnimation implements GlideAnimation { + private final GlideAnimation defaultAnimation; + private final int duration; + + /** + * Constructor that takes a default animation and a duration in milliseconds that the cross fade animation should + * last. + * @param duration The duration that the cross fade animation should run if there is something to cross fade from + * when a new {@link Drawable} is set. + */ + public DrawableCrossFadeViewAnimation(GlideAnimation defaultAnimation, int duration) { + this.defaultAnimation = defaultAnimation; + this.duration = duration; + } + + /** + * Animates from the previous drawable to the current drawable in one of two ways. + * + *
    + *
  1. Using the default animation provided in the constructor if the previous drawable is null
  2. + *
  3. Using the cross fade animation with the duration provided in the constructor if the previous + * drawable is non null
  4. + *
+ * + * @param current {@inheritDoc} + * @param adapter {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean animate(T current, ViewAdapter adapter) { + Drawable previous = adapter.getCurrentDrawable(); + if (previous != null) { + TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current }); + transitionDrawable.setCrossFadeEnabled(true); + transitionDrawable.startTransition(duration); + adapter.setDrawable(transitionDrawable); + return true; + } else { + defaultAnimation.animate(current, adapter); + return false; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimation.java b/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimation.java new file mode 100755 index 0000000..7ecac9a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimation.java @@ -0,0 +1,55 @@ +package com.example.bumptech.glide.request.animation; + +import android.graphics.drawable.Drawable; +import android.view.View; + +/** + * An interface that allows a transformation to be applied to {@link View}s in + * {@link com.bumptech.glide.request.target.Target}s in across resource types. Targets that wrap views will be able to + * provide all of the necessary arguments and start the animation. Those that do not will be unable to provide the + * necessary arguments and will therefore be forced to ignore the animation. This interface is a compromise that + * allows view animations in Glide's complex world of arbitrary resource types and arbitrary target types. + * + * @param The type of the resource that should be animated to. + */ +public interface GlideAnimation { + + /** + * An interface wrapping a view that exposes the necessary methods to run the various types of android animations + * ({@link ViewAnimation}, + * {@link ViewPropertyAnimation} and animated + * {@link Drawable}s). + */ + interface ViewAdapter { + /** + * Returns the wrapped {@link View}. + */ + View getView(); + + /** + * Returns the current drawable being displayed in the view, or null if no such drawable exists (or one cannot + * be retrieved). + */ + Drawable getCurrentDrawable(); + + /** + * Sets the current drawable (usually an animated drawable) to display in the wrapped view. + * + * @param drawable The drawable to display in the wrapped view. + */ + void setDrawable(Drawable drawable); + } + + /** + * Animates from the previous {@link Drawable} that is currently being displayed in the + * given view, if not null, to the new resource that should be displayed in the view. + * + * @param current The new resource that will be displayed in the view. + * @param adapter The {@link ViewAdapter} wrapping a view that + * can at least return an {@link View} from + * {@link ViewAdapter#getView()}. + * @return True if int he process of running the animation the new resource was set on the view, false if the caller + * needs to manually set the current resource on the view. + */ + boolean animate(R current, ViewAdapter adapter); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimationFactory.java b/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimationFactory.java new file mode 100755 index 0000000..beb40a4 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/GlideAnimationFactory.java @@ -0,0 +1,17 @@ +package com.example.bumptech.glide.request.animation; + +/** + * A factory class that can produce different {@link GlideAnimation}s based on the + * state of the request. + * @param The type of resource that needs to be animated into the target. + */ +public interface GlideAnimationFactory { + + /** + * Returns a new {@link GlideAnimation}. + * + * @param isFromMemoryCache True if this will be an animation for a resource that was loaded from the memory cache. + * @param isFirstResource True if this is the first resource to be loaded into the target. + */ + GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/NoAnimation.java b/core/src/main/java/com/example/bumptech/glide/request/animation/NoAnimation.java new file mode 100755 index 0000000..91625c2 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/NoAnimation.java @@ -0,0 +1,47 @@ +package com.example.bumptech.glide.request.animation; + +/** + * A simple {@link GlideAnimation} that performs no actions. + * + * @param animated resource type + */ +public class NoAnimation implements GlideAnimation { + private static final NoAnimation NO_ANIMATION = new NoAnimation(); + @SuppressWarnings("rawtypes") + private static final GlideAnimationFactory NO_ANIMATION_FACTORY = new NoAnimationFactory(); + + /** + * A factory that always returns the same {@link NoAnimation}. + */ + public static class NoAnimationFactory implements GlideAnimationFactory { + @SuppressWarnings("unchecked") + @Override + public GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource) { + return (GlideAnimation) NO_ANIMATION; + } + } + + /** + * Returns an instance of a factory that produces {@link NoAnimation}s. + */ + @SuppressWarnings("unchecked") + public static GlideAnimationFactory getFactory() { + return (GlideAnimationFactory) NO_ANIMATION_FACTORY; + } + + /** + * Returns an instance of {@link NoAnimation}. + */ + @SuppressWarnings("unchecked") + public static GlideAnimation get() { + return (GlideAnimation) NO_ANIMATION; + } + + /** + * Performs no animation and always returns {@code false}. + */ + @Override + public boolean animate(Object current, ViewAdapter adapter) { + return false; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimation.java b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimation.java new file mode 100755 index 0000000..27d516b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimation.java @@ -0,0 +1,49 @@ +package com.example.bumptech.glide.request.animation; + +import android.view.View; +import android.view.animation.Animation; + +/** + * A {@link GlideAnimation GlideAnimation} that can apply a + * {@link Animation Animation} to a {@link View View} using + * {@link View#startAnimation(Animation) View.startAnimation}. + * + * @param The type of the resource displayed in the view that is animated + */ +public class ViewAnimation implements GlideAnimation { + + private final AnimationFactory animationFactory; + + /** + * Constructs a new ViewAnimation that will start the given {@link Animation}. + */ + ViewAnimation(AnimationFactory animationFactory) { + this.animationFactory = animationFactory; + } + + /** + * Always clears the current animation on the view using {@link View#clearAnimation()}, then + * starts the {@link Animation} given in the constructor using + * {@link View#startAnimation(Animation)} and then returns {@code false} because + * the animation does not actually set the current resource on the view. + * + * @param current {@inheritDoc} + * @param adapter {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean animate(R current, ViewAdapter adapter) { + View view = adapter.getView(); + if (view != null) { + view.clearAnimation(); + Animation animation = animationFactory.build(); + view.startAnimation(animation); + } + + return false; + } + + interface AnimationFactory { + Animation build(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimationFactory.java b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimationFactory.java new file mode 100755 index 0000000..871de19 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewAnimationFactory.java @@ -0,0 +1,78 @@ +package com.example.bumptech.glide.request.animation; + +import android.content.Context; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +/** + * A {@link GlideAnimationFactory} that produces + * {@link ViewAnimation}s. + * + * @param The type of the resource displayed in the view that is animated + */ +public class ViewAnimationFactory implements GlideAnimationFactory { + private final ViewAnimation.AnimationFactory animationFactory; + private GlideAnimation glideAnimation; + + public ViewAnimationFactory(Animation animation) { + this(new ConcreteAnimationFactory(animation)); + } + + public ViewAnimationFactory(Context context, int animationId) { + this(new ResourceAnimationFactory(context, animationId)); + } + + ViewAnimationFactory(ViewAnimation.AnimationFactory animationFactory) { + this.animationFactory = animationFactory; + } + + /** + * Returns a new {@link GlideAnimation} for the given arguments. If + * isFromMemoryCache is {@code true} or isFirstImage is {@code false}, returns a + * {@link NoAnimation} and otherwise returns a new + * {@link ViewAnimation}. + * + * @param isFromMemoryCache {@inheritDoc} + * @param isFirstResource {@inheritDoc} + */ + @Override + public GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource) { + if (isFromMemoryCache || !isFirstResource) { + return NoAnimation.get(); + } + + if (glideAnimation == null) { + glideAnimation = new ViewAnimation(animationFactory); + } + + return glideAnimation; + } + + private static class ConcreteAnimationFactory implements ViewAnimation.AnimationFactory { + private final Animation animation; + + public ConcreteAnimationFactory(Animation animation) { + this.animation = animation; + } + + @Override + public Animation build() { + return animation; + } + } + + private static class ResourceAnimationFactory implements ViewAnimation.AnimationFactory { + private final Context context; + private final int animationId; + + public ResourceAnimationFactory(Context context, int animationId) { + this.context = context.getApplicationContext(); + this.animationId = animationId; + } + + @Override + public Animation build() { + return AnimationUtils.loadAnimation(context, animationId); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimation.java b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimation.java new file mode 100755 index 0000000..0f7a66a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimation.java @@ -0,0 +1,57 @@ +package com.example.bumptech.glide.request.animation; + +import android.view.View; + +/** + * A {@link GlideAnimation GlideAnimation} that accepts an interface + * that can apply an animation like a {@link android.view.ViewPropertyAnimator} + * or a {@link android.animation.ObjectAnimator} to an {@link View}. + * + * @param The type of the resource displayed in the view that is animated + */ +public class ViewPropertyAnimation implements GlideAnimation { + + private final Animator animator; + + /** + * Constructor for a view property animation that takes an + * {@link Animator} interface that can apply an animation + * to a view. + * + * @param animator The animator to use. + */ + public ViewPropertyAnimation(Animator animator) { + this.animator = animator; + } + + /** + * Always applies the {@link Animator} given in the + * constructor to the given view and returns {@code false} because the animator cannot set the new resource on + * the view. + * + * @param current {@inheritDoc} + * @param adapter {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean animate(R current, ViewAdapter adapter) { + final View view = adapter.getView(); + if (view != null) { + animator.animate(adapter.getView()); + } + return false; + } + + /** + * An interface that allows an animation to be applied on or started from an {@link View}. + */ + public interface Animator { + /** + * Starts an animation on the given {@link View}. + * + * @param view The view to animate. + */ + void animate(View view); + } + +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimationFactory.java b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimationFactory.java new file mode 100755 index 0000000..74e11ad --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/animation/ViewPropertyAnimationFactory.java @@ -0,0 +1,34 @@ +package com.example.bumptech.glide.request.animation; + +/** + * A {@link GlideAnimationFactory} that produces ViewPropertyAnimations. + * + * @param The type of the resource displayed in the view that is animated + */ +public class ViewPropertyAnimationFactory implements GlideAnimationFactory { + private final ViewPropertyAnimation.Animator animator; + private ViewPropertyAnimation animation; + + public ViewPropertyAnimationFactory(ViewPropertyAnimation.Animator animator) { + this.animator = animator; + } + + /** + * Returns a new {@link GlideAnimation} for the given arguments. If + * isMemoryCache is {@code true} or isFirstImage is {@code false}, returns a + * {@link NoAnimation} and otherwise returns a new + * {@link ViewPropertyAnimation} for the + * {@link ViewPropertyAnimation.Animator} provided in the constructor. + */ + @Override + public GlideAnimation build(boolean isFromMemoryCache, boolean isFirstResource) { + if (isFromMemoryCache || !isFirstResource) { + return NoAnimation.get(); + } + if (animation == null) { + animation = new ViewPropertyAnimation(animator); + } + + return animation; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/AppWidgetTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/AppWidgetTarget.java new file mode 100755 index 0000000..6c22033 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/AppWidgetTarget.java @@ -0,0 +1,137 @@ +package com.example.bumptech.glide.request.target; + +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +import com.example.bumptech.glide.request.animation.GlideAnimation; + + +/** + * This class is used in order to display downloaded Bitmap inside an ImageView + * of an AppWidget through RemoteViews. + * + *

+ * Note - For cancellation to work correctly, you must pass in the same instance of this class for every subsequent + * load. + *

+ */ +public class AppWidgetTarget extends SimpleTarget { + + private final int[] widgetIds; + private final ComponentName componentName; + private final RemoteViews remoteViews; + private final Context context; + private final int viewId; + + /** + * Constructor using an int array of widgetIds to get a handle on the Widget in order to update it. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param width Desired width in pixels of the bitmap that will be loaded. (Needs to be manually set + * because of RemoteViews limitations.) + * @param height Desired height in pixels of the bitmap that will be loaded. (Needs to be manually set + * because of RemoteViews limitations.) + * @param widgetIds The int[] that contains the widget ids of an application. + */ + public AppWidgetTarget(Context context, RemoteViews remoteViews, int viewId, int width, int height, + int... widgetIds) { + super(width, height); + if (context == null) { + throw new NullPointerException("Context can not be null!"); + } + if (widgetIds == null) { + throw new NullPointerException("WidgetIds can not be null!"); + } + if (widgetIds.length == 0) { + throw new IllegalArgumentException("WidgetIds must have length > 0"); + } + if (remoteViews == null) { + throw new NullPointerException("RemoteViews object can not be null!"); + } + this.context = context; + this.remoteViews = remoteViews; + this.viewId = viewId; + this.widgetIds = widgetIds; + componentName = null; + } + + /** + * Constructor using an int array of widgetIds to get a handle on the Widget in order to update it that uses + * {@link #SIZE_ORIGINAL} as the target width and height. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param widgetIds The int[] that contains the widget ids of an application. + */ + public AppWidgetTarget(Context context, RemoteViews remoteViews, int viewId, int... widgetIds) { + this(context, remoteViews, viewId, SIZE_ORIGINAL, SIZE_ORIGINAL, widgetIds); + } + + /** + * Constructor using a ComponentName to get a handle on the Widget in order to update it. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param width Desired width in pixels of the bitmap that will be loaded. (Needs to be manually set + * because of RemoteViews limitations.) + * @param height Desired height in pixels of the bitmap that will be loaded. (Needs to be manually set + * because of RemoteViews limitations.) + * @param componentName The ComponentName that refers to our AppWidget. + */ + public AppWidgetTarget(Context context, RemoteViews remoteViews, int viewId, int width, int height, + ComponentName componentName) { + super(width, height); + if (context == null) { + throw new NullPointerException("Context can not be null!"); + } + if (componentName == null) { + throw new NullPointerException("ComponentName can not be null!"); + } + if (remoteViews == null) { + throw new NullPointerException("RemoteViews object can not be null!"); + } + this.context = context; + this.remoteViews = remoteViews; + this.viewId = viewId; + this.componentName = componentName; + widgetIds = null; + } + + /** + * Constructor using a ComponentName, when override has been set to get a handle on the Widget in order to update + * it that uses {@link #SIZE_ORIGINAL} as the target width and height. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param componentName The ComponentName that refers to our AppWidget. + */ + public AppWidgetTarget(Context context, RemoteViews remoteViews, int viewId, ComponentName componentName) { + this(context, remoteViews, viewId, SIZE_ORIGINAL, SIZE_ORIGINAL, componentName); + } + + /** + * Updates the AppWidget after the ImageView has loaded the Bitmap. + */ + private void update() { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this.context); + if (this.componentName != null) { + appWidgetManager.updateAppWidget(this.componentName, this.remoteViews); + } else { + appWidgetManager.updateAppWidget(this.widgetIds, this.remoteViews); + } + } + + @Override + public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { + this.remoteViews.setImageViewBitmap(this.viewId, resource); + this.update(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/BaseTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/BaseTarget.java new file mode 100755 index 0000000..0dd1900 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/BaseTarget.java @@ -0,0 +1,93 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.drawable.Drawable; + +import com.example.bumptech.glide.request.Request; + + +/** + * A base {@link Target} for loading {@link com.bumptech.glide.load.engine.Resource}s that provides basic or empty + * implementations for most methods. + * + *

+ * For maximum efficiency, clear this target when you have finished using or displaying the + * {@link com.bumptech.glide.load.engine.Resource} loaded into it using + * {@link com.bumptech.glide.Glide#clear(Target)}. + *

+ * + *

+ * For loading {@link com.bumptech.glide.load.engine.Resource}s into {@link android.view.View}s, + * {@link ViewTarget} or {@link ImageViewTarget} + * are preferable. + *

+ * + * @param The type of resource that will be received by this target. + */ +public abstract class BaseTarget implements Target { + + private Request request; + + /** + * {@inheritDoc} + */ + @Override + public void setRequest(Request request) { + this.request = request; + } + + /** + * {@inheritDoc} + */ + @Override + public Request getRequest() { + return request; + } + + /** + * {@inheritDoc} + */ + @Override + public void onLoadCleared(Drawable placeholder) { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void onLoadStarted(Drawable placeholder) { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void onStart() { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void onStop() { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void onDestroy() { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/BitmapImageViewTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/BitmapImageViewTarget.java new file mode 100755 index 0000000..90c55c1 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/BitmapImageViewTarget.java @@ -0,0 +1,27 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.Bitmap; +import android.widget.ImageView; + +/** + * A {@link Target} that can display an {@link Bitmap} in an + * {@link ImageView}. + * + * @see GlideDrawableImageViewTarget + */ +public class BitmapImageViewTarget extends ImageViewTarget { + public BitmapImageViewTarget(ImageView view) { + super(view); + } + + /** + * Sets the {@link Bitmap} on the view using + * {@link ImageView#setImageBitmap(Bitmap)}. + * + * @param resource The bitmap to display. + */ + @Override + protected void setResource(Bitmap resource) { + view.setImageBitmap(resource); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/DrawableImageViewTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/DrawableImageViewTarget.java new file mode 100755 index 0000000..03d2f22 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/DrawableImageViewTarget.java @@ -0,0 +1,18 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +/** + * A target for display {@link Drawable} objects in {@link ImageView}s. + */ +public class DrawableImageViewTarget extends ImageViewTarget { + public DrawableImageViewTarget(ImageView view) { + super(view); + } + + @Override + protected void setResource(Drawable resource) { + view.setImageDrawable(resource); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/GlideDrawableImageViewTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/GlideDrawableImageViewTarget.java new file mode 100755 index 0000000..52bc156 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/GlideDrawableImageViewTarget.java @@ -0,0 +1,97 @@ +package com.example.bumptech.glide.request.target; + +import android.widget.ImageView; + +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.example.bumptech.glide.request.animation.GlideAnimation; + + +/** + * A {@link Target} that can display an {@link android.graphics.drawable.Drawable} in + * an {@link ImageView}. + */ +public class GlideDrawableImageViewTarget extends ImageViewTarget { + private static final float SQUARE_RATIO_MARGIN = 0.05f; + private int maxLoopCount; + private GlideDrawable resource; + + /** + * Constructor for an {@link Target} that can display an + * {@link GlideDrawable} in an {@link ImageView}. + * + * @param view The view to display the drawable in. + */ + public GlideDrawableImageViewTarget(ImageView view) { + this(view, GlideDrawable.LOOP_FOREVER); + } + + /** + * Constructor for an {@link Target} that can display an + * {@link GlideDrawable} in an {@link ImageView}. + * + * @param view The view to display the drawable in. + * @param maxLoopCount A value to pass to to {@link GlideDrawable}s + * indicating how many times they should repeat their animation (if they have one). See + * {@link GlideDrawable#setLoopCount(int)}. + */ + public GlideDrawableImageViewTarget(ImageView view, int maxLoopCount) { + super(view); + this.maxLoopCount = maxLoopCount; + } + + /** + * {@inheritDoc} + * If no {@link GlideAnimation} is given or if the animation does not set the + * {@link android.graphics.drawable.Drawable} on the view, the drawable is set using + * {@link ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. + * + * @param resource {@inheritDoc} + * @param animation {@inheritDoc} + */ + @Override + public void onResourceReady(GlideDrawable resource, GlideAnimation animation) { + if (!resource.isAnimated()) { + //TODO: Try to generalize this to other sizes/shapes. + // This is a dirty hack that tries to make loading square thumbnails and then square full images less costly + // by forcing both the smaller thumb and the larger version to have exactly the same intrinsic dimensions. + // If a drawable is replaced in an ImageView by another drawable with different intrinsic dimensions, + // the ImageView requests a layout. Scrolling rapidly while replacing thumbs with larger images triggers + // lots of these calls and causes significant amounts of jank. + float viewRatio = view.getWidth() / (float) view.getHeight(); + float drawableRatio = resource.getIntrinsicWidth() / (float) resource.getIntrinsicHeight(); + if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN + && Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) { + resource = new SquaringDrawable(resource, view.getWidth()); + } + } + super.onResourceReady(resource, animation); + this.resource = resource; + resource.setLoopCount(maxLoopCount); + resource.start(); + } + + /** + * Sets the drawable on the view using + * {@link ImageView#setImageDrawable(android.graphics.drawable.Drawable)}. + * + * @param resource The {@link android.graphics.drawable.Drawable} to display in the view. + */ + @Override + protected void setResource(GlideDrawable resource) { + view.setImageDrawable(resource); + } + + @Override + public void onStart() { + if (resource != null) { + resource.start(); + } + } + + @Override + public void onStop() { + if (resource != null) { + resource.stop(); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTarget.java new file mode 100755 index 0000000..d9694cf --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTarget.java @@ -0,0 +1,84 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import com.example.bumptech.glide.request.animation.GlideAnimation; + + +/** + * A base {@link Target} for displaying resources in + * {@link ImageView}s. + * + * @param The type of resource that this target will display in the wrapped {@link ImageView}. + */ +public abstract class ImageViewTarget extends ViewTarget implements GlideAnimation.ViewAdapter { + + public ImageViewTarget(ImageView view) { + super(view); + } + + /** + * Returns the current {@link Drawable} being displayed in the view using + * {@link ImageView#getDrawable()}. + */ + @Override + public Drawable getCurrentDrawable() { + return view.getDrawable(); + } + + /** + * Sets the given {@link Drawable} on the view using + * {@link ImageView#setImageDrawable(Drawable)}. + * + * @param drawable {@inheritDoc} + */ + @Override + public void setDrawable(Drawable drawable) { + view.setImageDrawable(drawable); + } + + /** + * Sets the given {@link Drawable} on the view using + * {@link ImageView#setImageDrawable(Drawable)}. + * + * @param placeholder {@inheritDoc} + */ + @Override + public void onLoadStarted(Drawable placeholder) { + view.setImageDrawable(placeholder); + } + + /** + * Sets the given {@link Drawable} on the view using + * {@link ImageView#setImageDrawable(Drawable)}. + * + * @param errorDrawable {@inheritDoc} + */ + @Override + public void onLoadFailed(Exception e, Drawable errorDrawable) { + view.setImageDrawable(errorDrawable); + } + + /** + * Sets the given {@link Drawable} on the view using + * {@link ImageView#setImageDrawable(Drawable)}. + * + * @param placeholder {@inheritDoc} + */ + @Override + public void onLoadCleared(Drawable placeholder) { + view.setImageDrawable(placeholder); + } + + @Override + public void onResourceReady(Z resource, GlideAnimation glideAnimation) { + if (glideAnimation == null || !glideAnimation.animate(resource, this)) { + setResource(resource); + } + } + + protected abstract void setResource(Z resource); + +} + diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTargetFactory.java b/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTargetFactory.java new file mode 100755 index 0000000..88e0c1c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/ImageViewTargetFactory.java @@ -0,0 +1,28 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.widget.ImageView; + +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; + +/** + * A factory responsible for producing the correct type of {@link Target} for a given + * {@link android.view.View} subclass. + */ +public class ImageViewTargetFactory { + + @SuppressWarnings("unchecked") + public Target buildTarget(ImageView view, Class clazz) { + if (GlideDrawable.class.isAssignableFrom(clazz)) { + return (Target) new GlideDrawableImageViewTarget(view); + } else if (Bitmap.class.equals(clazz)) { + return (Target) new BitmapImageViewTarget(view); + } else if (Drawable.class.isAssignableFrom(clazz)) { + return (Target) new DrawableImageViewTarget(view); + } else { + throw new IllegalArgumentException("Unhandled class: " + clazz + + ", try .as*(Class).transcode(ResourceTranscoder)"); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/NotificationTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/NotificationTarget.java new file mode 100755 index 0000000..1e7a788 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/NotificationTarget.java @@ -0,0 +1,91 @@ +package com.example.bumptech.glide.request.target; + + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +import com.example.bumptech.glide.request.animation.GlideAnimation; + + +/** + * This class is used to display downloaded Bitmap inside an ImageView of a Notification through RemoteViews. + * + *

+ * Note - For cancellation to work correctly, you must pass in the same instance of this class for every subsequent + * load. + *

+ */ +public class NotificationTarget extends SimpleTarget { + + private final RemoteViews remoteViews; + private final Context context; + private final int notificationId; + private final Notification notification; + private final int viewId; + + /** + * Constructor using a Notification object and a notificationId to get a handle on the Notification in order to + * update it that uses {@link #SIZE_ORIGINAL} as the target width and height. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param notification The Notification object that we want to update. + * @param notificationId The notificationId of the Notification that we want to load the Bitmap. + */ + public NotificationTarget(Context context, RemoteViews remoteViews, int viewId, Notification notification, + int notificationId) { + this(context, remoteViews, viewId, SIZE_ORIGINAL, SIZE_ORIGINAL, notification, notificationId); + } + + /** + * Constructor using a Notification object and a notificationId to get a handle on the Notification in order to + * update it. + * + * @param context Context to use in the AppWidgetManager initialization. + * @param remoteViews RemoteViews object which contains the ImageView that will load the bitmap. + * @param viewId The id of the ImageView view that will load the image. + * @param width Desired width of the bitmap that will be loaded.(Need to be manually set + * because of RemoteViews limitations.) + * @param height Desired height of the bitmap that will be loaded. (Need to be manually set + * because of RemoteViews limitations.) + * @param notification The Notification object that we want to update. + * @param notificationId The notificationId of the Notification that we want to load the Bitmap. + */ + public NotificationTarget(Context context, RemoteViews remoteViews, int viewId, int width, int height, + Notification notification, int notificationId) { + super(width, height); + if (context == null) { + throw new NullPointerException("Context must not be null!"); + } + if (notification == null) { + throw new NullPointerException("Notification object can not be null!"); + } + if (remoteViews == null) { + throw new NullPointerException("RemoteViews object can not be null!"); + } + this.context = context; + this.viewId = viewId; + this.notification = notification; + this.notificationId = notificationId; + this.remoteViews = remoteViews; + } + + /** + * Updates the Notification after the Bitmap resource is loaded. + */ + private void update() { + NotificationManager manager = (NotificationManager) + this.context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.notify(this.notificationId, this.notification); + } + + @Override + public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { + this.remoteViews.setImageViewBitmap(this.viewId, resource); + this.update(); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/PreloadTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/PreloadTarget.java new file mode 100755 index 0000000..950abd8 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/PreloadTarget.java @@ -0,0 +1,34 @@ +package com.example.bumptech.glide.request.target; + + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.request.animation.GlideAnimation; + +/** + * A one time use {@link Target} class that loads a resource into memory and then + * clears itself. + * + * @param The type of resource that will be loaded into memory. + */ +public final class PreloadTarget extends SimpleTarget { + + /** + * Returns a PreloadTarget. + * + * @param width The width in pixels of the desired resource. + * @param height The height in pixels of the desired resource. + * @param The type of the desired resource. + */ + public static PreloadTarget obtain(int width, int height) { + return new PreloadTarget(width, height); + } + + private PreloadTarget(int width, int height) { + super(width, height); + } + + @Override + public void onResourceReady(Z resource, GlideAnimation glideAnimation) { + Glide.clear(this); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/SimpleTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/SimpleTarget.java new file mode 100755 index 0000000..589e10e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/SimpleTarget.java @@ -0,0 +1,64 @@ +package com.example.bumptech.glide.request.target; + +import com.example.bumptech.glide.util.Util; + +/** + * A simple {@link Target} base class with default (usually noop) implementations + * of non essential methods that allows the caller to specify an exact width/height. Typicaly use cases look something + * like this: + *
+ * 
+ * Glide.load("http://somefakeurl.com/fakeImage.jpeg")
+ *      .asBitmap()
+ *      .fitCenter()
+ *      .into(new SimpleTarget(250, 250) {
+ *
+ *          {@literal @Override}
+ *          public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) {
+ *              // Do something with bitmap here.
+ *          }
+ *
+ *      });
+ * }
+ * 
+ * 
+ * + * @param The type of resource that this target will receive. + */ +public abstract class SimpleTarget extends BaseTarget { + private final int width; + private final int height; + + /** + * Constructor for the target that uses {@link Target#SIZE_ORIGINAL} as the target width and height. + */ + public SimpleTarget() { + this(SIZE_ORIGINAL, SIZE_ORIGINAL); + } + + /** + * Constructor for the target that takes the desired dimensions of the decoded and/or transformed resource. + * + * @param width The width in pixels of the desired resource. + * @param height The height in pixels of the desired resource. + */ + public SimpleTarget(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Immediately calls the given callback with the sizes given in the constructor. + * + * @param cb {@inheritDoc} + */ + @Override + public final void getSize(SizeReadyCallback cb) { + if (!Util.isValidDimensions(width, height)) { + throw new IllegalArgumentException("Width and height must both be > 0 or Target#SIZE_ORIGINAL, but given" + + " width: " + width + " and height: " + height + ", either provide dimensions in the constructor" + + " or call override()"); + } + cb.onSizeReady(width, height); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/SizeReadyCallback.java b/core/src/main/java/com/example/bumptech/glide/request/target/SizeReadyCallback.java new file mode 100755 index 0000000..9416989 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/SizeReadyCallback.java @@ -0,0 +1,17 @@ +package com.example.bumptech.glide.request.target; + +/** + * A callback that must be called when the target has determined its size. For fixed size targets it can + * be called synchronously. + */ +public interface SizeReadyCallback { + /** + * A callback called on the main thread. + * + * @param width The width in pixels of the target, or {@link Target#SIZE_ORIGINAL} to indicate that we want the + * resource at its original width. + * @param height The height in pixels of the target, or {@link Target#SIZE_ORIGINAL} to indicate that we want the + * resource at its original height. + */ + void onSizeReady(int width, int height); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/SquaringDrawable.java b/core/src/main/java/com/example/bumptech/glide/request/target/SquaringDrawable.java new file mode 100755 index 0000000..9b9e95b --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/SquaringDrawable.java @@ -0,0 +1,237 @@ +package com.example.bumptech.glide.request.target; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable; + +/** + * A wrapper drawable to square the wrapped drawable so that it expands to fill a square with exactly the given side + * length. The goal of this drawable is to ensure that square thumbnail drawables always match the size of the view + * they will be displayed in to avoid a costly requestLayout call. This class should not be used with views or drawables + * that are not square. + */ +public class SquaringDrawable extends GlideDrawable { + private GlideDrawable wrapped; + private State state; + private boolean mutated; + + public SquaringDrawable(GlideDrawable wrapped, int side) { + this(new State(wrapped.getConstantState(), side), wrapped, null /*res*/); + } + + SquaringDrawable(State state, GlideDrawable wrapped, Resources res) { + this.state = state; + if (wrapped == null) { + if (res != null) { + this.wrapped = (GlideDrawable) state.wrapped.newDrawable(res); + } else { + this.wrapped = (GlideDrawable) state.wrapped.newDrawable(); + } + } else { + this.wrapped = wrapped; + } + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + wrapped.setBounds(left, top, right, bottom); + } + + @Override + public void setBounds(Rect bounds) { + super.setBounds(bounds); + wrapped.setBounds(bounds); + } + + @Override + public void setChangingConfigurations(int configs) { + wrapped.setChangingConfigurations(configs); + } + + @Override + public int getChangingConfigurations() { + return wrapped.getChangingConfigurations(); + } + + @Override + public void setDither(boolean dither) { + wrapped.setDither(dither); + } + + @Override + public void setFilterBitmap(boolean filter) { + wrapped.setFilterBitmap(filter); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public Drawable.Callback getCallback() { + return wrapped.getCallback(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + @Override + public int getAlpha() { + return wrapped.getAlpha(); + } + + @Override + public void setColorFilter(int color, PorterDuff.Mode mode) { + wrapped.setColorFilter(color, mode); + } + + @Override + public void clearColorFilter() { + wrapped.clearColorFilter(); + } + + @Override + public Drawable getCurrent() { + return wrapped.getCurrent(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return wrapped.setVisible(visible, restart); + } + + @Override + public int getIntrinsicWidth() { + return state.side; + } + + @Override + public int getIntrinsicHeight() { + return state.side; + } + + @Override + public int getMinimumWidth() { + return wrapped.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return wrapped.getMinimumHeight(); + } + + @Override + public boolean getPadding(Rect padding) { + return wrapped.getPadding(padding); + } + + @Override + public void invalidateSelf() { + super.invalidateSelf(); + wrapped.invalidateSelf(); + } + + @Override + public void unscheduleSelf(Runnable what) { + super.unscheduleSelf(what); + wrapped.unscheduleSelf(what); + } + + @Override + public void scheduleSelf(Runnable what, long when) { + super.scheduleSelf(what, when); + wrapped.scheduleSelf(what, when); + } + + @Override + public void draw(Canvas canvas) { + wrapped.draw(canvas); + } + + @Override + public void setAlpha(int i) { + wrapped.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + wrapped.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return wrapped.getOpacity(); + } + + @Override + public boolean isAnimated() { + return wrapped.isAnimated(); + } + + @Override + public void setLoopCount(int loopCount) { + wrapped.setLoopCount(loopCount); + } + + @Override + public void start() { + wrapped.start(); + } + + @Override + public void stop() { + wrapped.stop(); + } + + @Override + public boolean isRunning() { + return wrapped.isRunning(); + } + + @Override + public Drawable mutate() { + if (!mutated && super.mutate() == this) { + wrapped = (GlideDrawable) wrapped.mutate(); + state = new State(state); + mutated = true; + } + return this; + } + + @Override + public Drawable.ConstantState getConstantState() { + return state; + } + + static class State extends Drawable.ConstantState { + private final Drawable.ConstantState wrapped; + private final int side; + + State(State other) { + this(other.wrapped, other.side); + } + + State(Drawable.ConstantState wrapped, int side) { + this.wrapped = wrapped; + this.side = side; + } + + @Override + public Drawable newDrawable() { + return newDrawable(null /*res*/); + } + + @Override + public Drawable newDrawable(Resources res) { + return new SquaringDrawable(this, null /*wrapped*/, res); + } + + @Override + public int getChangingConfigurations() { + return 0; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/Target.java b/core/src/main/java/com/example/bumptech/glide/request/target/Target.java new file mode 100755 index 0000000..138e198 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/Target.java @@ -0,0 +1,97 @@ +package com.example.bumptech.glide.request.target; + +import android.graphics.drawable.Drawable; + +import com.example.bumptech.glide.manager.LifecycleListener; +import com.example.bumptech.glide.request.Request; +import com.example.bumptech.glide.request.animation.GlideAnimation; + + +/** + * An interface that Glide can load a resource into and notify of relevant lifecycle events during a load. + * + *

+ * The lifecycle events in this class are as follows: + *

    + *
  • onLoadStarted
  • + *
  • onResourceReady
  • + *
  • onLoadCleared
  • + *
  • onLoadFailed
  • + *
+ * + * The typical lifecycle is onLoadStarted -> onResourceReady or onLoadFailed -> onLoadCleared. However, there are no + * guarantees. onLoadStarted may not be called if the resource is in memory or if the load will fail because of a + * null model object. onLoadCleared similarly may never be called if the target is never cleared. See the docs for + * the individual methods for details. + *

+ * + * @param The type of resource the target can display. + */ +public interface Target extends LifecycleListener { + /** + * Indicates that we want the resource in its original unmodified width and/or height. + */ + int SIZE_ORIGINAL = Integer.MIN_VALUE; + + /** + * A lifecycle callback that is called when a load is started. + * + *

+ * Note - This may not be called for every load, it is possible for example for loads to fail before the load + * starts (when the model object is null). + *

+ * + *

+ * Note - This method may be called multiple times before any other lifecycle method is called. Loads can be + * paused and restarted due to lifecycle or connectivity events and each restart may cause a call here. + *

+ * + * @param placeholder The placeholder drawable to optionally show, or null. + */ + void onLoadStarted(Drawable placeholder); + + /** + * A lifecycle callback that is called when a load fails. + * + *

+ * Note - This may be called before {@link #onLoadStarted(Drawable)} if the model + * object is null. + *

+ * + * @param e The exception causing the load to fail, or null if no exception occurred (usually because a decoder + * simply returned null). + * @param errorDrawable The error drawable to optionally show, or null. + */ + void onLoadFailed(Exception e, Drawable errorDrawable); + + /** + * The method that will be called when the resource load has finished. + * + * @param resource the loaded resource. + */ + void onResourceReady(R resource, GlideAnimation glideAnimation); + + /** + * A lifecycle callback that is called when a load is cancelled and its resources are freed. + * + * @param placeholder The placeholder drawable to optionally show, or null. + */ + void onLoadCleared(Drawable placeholder); + + /** + * A method to retrieve the size of this target. + * + * @param cb The callback that must be called when the size of the target has been determined + */ + void getSize(SizeReadyCallback cb); + + /** + * Sets the current request for this target to retain, should not be called outside of Glide. + */ + void setRequest(Request request); + + /** + * Retrieves the current request for this target, should not be called outside of Glide. + */ + Request getRequest(); +} diff --git a/core/src/main/java/com/example/bumptech/glide/request/target/ViewTarget.java b/core/src/main/java/com/example/bumptech/glide/request/target/ViewTarget.java new file mode 100755 index 0000000..69b6a07 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/request/target/ViewTarget.java @@ -0,0 +1,299 @@ +package com.example.bumptech.glide.request.target; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.util.Log; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.view.WindowManager; + + +import com.example.bumptech.glide.request.Request; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * A base {@link Target} for loading {@link android.graphics.Bitmap}s into {@link View}s that provides default + * implementations for most most methods and can determine the size of views using a + * {@link ViewTreeObserver.OnDrawListener}. + * + *

+ * To detect {@link View} reuse in {@link android.widget.ListView} or any {@link android.view.ViewGroup} that reuses + * views, this class uses the {@link View#setTag(Object)} method to store some metadata so that if a view is reused, + * any previous loads or resources from previous loads can be cancelled or reused. + *

+ * + *

+ * Any calls to {@link View#setTag(Object)}} on a View given to this class will result in excessive allocations and + * and/or {@link IllegalArgumentException}s. If you must call {@link View#setTag(Object)} on a view, consider + * using {@link BaseTarget} or {@link SimpleTarget} instead. + *

+ * + * @param The specific subclass of view wrapped by this target. + * @param The resource type this target will receive. + */ +public abstract class ViewTarget extends BaseTarget { + private static final String TAG = "ViewTarget"; + private static boolean isTagUsedAtLeastOnce = false; + private static Integer tagId = null; + + protected final T view; + private final SizeDeterminer sizeDeterminer; + + /** + * Sets the android resource id to use in conjunction with {@link View#setTag(int, Object)} + * to store temporary state allowing loads to be automatically cancelled and resources re-used + * in scrolling lists. + * + *

+ * If no tag id is set, Glide will use {@link View#setTag(Object)}. + *

+ * + *

+ * Warning: prior to Android 4.0 tags were stored in a static map. Using this method prior + * to Android 4.0 may cause memory leaks and isn't recommended. If you do use this method + * on older versions, be sure to call {@link com.bumptech.glide.Glide#clear(View)} on any view + * you start a load into to ensure that the static state is removed. + *

+ * + * @param tagId The android resource to use. + */ + public static void setTagId(int tagId) { + if (ViewTarget.tagId != null || isTagUsedAtLeastOnce) { + throw new IllegalArgumentException("You cannot set the tag id more than once or change" + + " the tag id after the first request has been made"); + } + ViewTarget.tagId = tagId; + } + + public ViewTarget(T view) { + if (view == null) { + throw new NullPointerException("View must not be null!"); + } + this.view = view; + sizeDeterminer = new SizeDeterminer(view); + } + + /** + * Returns the wrapped {@link View}. + */ + public T getView() { + return view; + } + + /** + * Determines the size of the view by first checking {@link View#getWidth()} and + * {@link View#getHeight()}. If one or both are zero, it then checks the view's + * {@link LayoutParams}. If one or both of the params width and height are less than or + * equal to zero, it then adds an {@link ViewTreeObserver.OnPreDrawListener} which waits until the view + * has been measured before calling the callback with the view's drawn width and height. + * + * @param cb {@inheritDoc} + */ + @Override + public void getSize(SizeReadyCallback cb) { + sizeDeterminer.getSize(cb); + } + + /** + * Stores the request using {@link View#setTag(Object)}. + * + * @param request {@inheritDoc} + */ + @Override + public void setRequest(Request request) { + setTag(request); + } + + /** + * Returns any stored request using {@link View#getTag()}. + * + *

+ * For Glide to function correctly, Glide must be the only thing that calls {@link View#setTag(Object)}. If the + * tag is cleared or set to another object type, Glide will not be able to retrieve and cancel previous loads + * which will not only prevent Glide from reusing resource, but will also result in incorrect images being + * loaded and lots of flashing of images in lists. As a result, this will throw an + * {@link IllegalArgumentException} if {@link View#getTag()}} returns a non null object + * that is not an {@link Request}. + *

+ */ + @Override + public Request getRequest() { + Object tag = getTag(); + Request request = null; + if (tag != null) { + if (tag instanceof Request) { + request = (Request) tag; + } else { + throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting"); + } + } + return request; + } + + private void setTag(Object tag) { + if (tagId == null) { + isTagUsedAtLeastOnce = true; + view.setTag(tag); + } else { + view.setTag(tagId, tag); + } + } + + private Object getTag() { + if (tagId == null) { + return view.getTag(); + } else { + return view.getTag(tagId); + } + } + + @Override + public String toString() { + return "Target for: " + view; + } + + private static class SizeDeterminer { + // Some negative sizes (WRAP_CONTENT) are valid, 0 is never valid. + private static final int PENDING_SIZE = 0; + + private final View view; + private final List cbs = new ArrayList(); + + private SizeDeterminerLayoutListener layoutListener; + private Point displayDimens; + + public SizeDeterminer(View view) { + this.view = view; + } + + private void notifyCbs(int width, int height) { + for (SizeReadyCallback cb : cbs) { + cb.onSizeReady(width, height); + } + cbs.clear(); + } + + private void checkCurrentDimens() { + if (cbs.isEmpty()) { + return; + } + + int currentWidth = getViewWidthOrParam(); + int currentHeight = getViewHeightOrParam(); + if (!isSizeValid(currentWidth) || !isSizeValid(currentHeight)) { + return; + } + + notifyCbs(currentWidth, currentHeight); + // Keep a reference to the layout listener and remove it here + // rather than having the observer remove itself because the observer + // we add the listener to will be almost immediately merged into + // another observer and will therefore never be alive. If we instead + // keep a reference to the listener and remove it here, we get the + // current view tree observer and should succeed. + ViewTreeObserver observer = view.getViewTreeObserver(); + if (observer.isAlive()) { + observer.removeOnPreDrawListener(layoutListener); + } + layoutListener = null; + } + + public void getSize(SizeReadyCallback cb) { + int currentWidth = getViewWidthOrParam(); + int currentHeight = getViewHeightOrParam(); + if (isSizeValid(currentWidth) && isSizeValid(currentHeight)) { + cb.onSizeReady(currentWidth, currentHeight); + } else { + // We want to notify callbacks in the order they were added and we only expect one or two callbacks to + // be added a time, so a List is a reasonable choice. + if (!cbs.contains(cb)) { + cbs.add(cb); + } + if (layoutListener == null) { + final ViewTreeObserver observer = view.getViewTreeObserver(); + layoutListener = new SizeDeterminerLayoutListener(this); + observer.addOnPreDrawListener(layoutListener); + } + } + } + + private int getViewHeightOrParam() { + final LayoutParams layoutParams = view.getLayoutParams(); + if (isSizeValid(view.getHeight())) { + return view.getHeight(); + } else if (layoutParams != null) { + return getSizeForParam(layoutParams.height, true /*isHeight*/); + } else { + return PENDING_SIZE; + } + } + + private int getViewWidthOrParam() { + final LayoutParams layoutParams = view.getLayoutParams(); + if (isSizeValid(view.getWidth())) { + return view.getWidth(); + } else if (layoutParams != null) { + return getSizeForParam(layoutParams.width, false /*isHeight*/); + } else { + return PENDING_SIZE; + } + } + + private int getSizeForParam(int param, boolean isHeight) { + if (param == LayoutParams.WRAP_CONTENT) { + Point displayDimens = getDisplayDimens(); + return isHeight ? displayDimens.y : displayDimens.x; + } else { + return param; + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + @SuppressWarnings("deprecation") + private Point getDisplayDimens() { + if (displayDimens != null) { + return displayDimens; + } + WindowManager windowManager = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = windowManager.getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + displayDimens = new Point(); + display.getSize(displayDimens); + } else { + displayDimens = new Point(display.getWidth(), display.getHeight()); + } + return displayDimens; + } + + private boolean isSizeValid(int size) { + return size > 0 || size == LayoutParams.WRAP_CONTENT; + } + + private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener { + private final WeakReference sizeDeterminerRef; + + public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) { + sizeDeterminerRef = new WeakReference(sizeDeterminer); + } + + @Override + public boolean onPreDraw() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "OnGlobalLayoutListener called listener=" + this); + } + SizeDeterminer sizeDeterminer = sizeDeterminerRef.get(); + if (sizeDeterminer != null) { + sizeDeterminer.checkCurrentDimens(); + } + return true; + } + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/signature/ApplicationVersionSignature.java b/core/src/main/java/com/example/bumptech/glide/signature/ApplicationVersionSignature.java new file mode 100755 index 0000000..638dd4d --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/signature/ApplicationVersionSignature.java @@ -0,0 +1,63 @@ +package com.example.bumptech.glide.signature; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import com.example.bumptech.glide.load.Key; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A utility class for obtaining a {@link Key} signature containing the application version + * name using {@link PackageInfo#versionCode}. + */ +public final class ApplicationVersionSignature { + private static final ConcurrentHashMap PACKAGE_NAME_TO_KEY = new ConcurrentHashMap(); + + /** + * Returns the signature {@link Key} for version code of the Application of the given + * Context. + */ + public static Key obtain(Context context) { + String packageName = context.getPackageName(); + Key result = PACKAGE_NAME_TO_KEY.get(packageName); + if (result == null) { + Key toAdd = obtainVersionSignature(context); + result = PACKAGE_NAME_TO_KEY.putIfAbsent(packageName, toAdd); + // There wasn't a previous mapping, so toAdd is now the Key. + if (result == null) { + result = toAdd; + } + } + + return result; + } + + // Visible for testing. + static void reset() { + PACKAGE_NAME_TO_KEY.clear(); + } + + private static Key obtainVersionSignature(Context context) { + PackageInfo pInfo = null; + try { + pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + } catch (PackageManager.NameNotFoundException e) { + // Should never happen. + e.printStackTrace(); + } + final String versionCode; + if (pInfo != null) { + versionCode = String.valueOf(pInfo.versionCode); + } else { + versionCode = UUID.randomUUID().toString(); + } + return new StringSignature(versionCode); + } + + private ApplicationVersionSignature() { + // Empty for visibility. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/signature/EmptySignature.java b/core/src/main/java/com/example/bumptech/glide/signature/EmptySignature.java new file mode 100755 index 0000000..75a429f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/signature/EmptySignature.java @@ -0,0 +1,27 @@ +package com.example.bumptech.glide.signature; + + +import com.example.bumptech.glide.load.Key; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; + +/** + * An empty key that is always equal to all other empty keys. + */ +public final class EmptySignature implements Key { + private static final EmptySignature EMPTY_KEY = new EmptySignature(); + + public static EmptySignature obtain() { + return EMPTY_KEY; + } + + private EmptySignature() { + // Empty. + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + // Do nothing. + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/signature/MediaStoreSignature.java b/core/src/main/java/com/example/bumptech/glide/signature/MediaStoreSignature.java new file mode 100755 index 0000000..d21b7f5 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/signature/MediaStoreSignature.java @@ -0,0 +1,76 @@ +package com.example.bumptech.glide.signature; +import com.example.bumptech.glide.load.Key; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + +/** + * A unique signature based on metadata data from the media store that detects common changes to media store files like + * edits, rotations, and temporary file replacement. + */ +public class MediaStoreSignature implements Key { + private final String mimeType; + private final long dateModified; + private final int orientation; + + /** + * Constructor for {@link MediaStoreSignature}. + * + * @param mimeType The mime type of the media store media. Ok to default to empty string "". See + * {@link android.provider.MediaStore.Images.ImageColumns#MIME_TYPE} or + * {@link android.provider.MediaStore.Video.VideoColumns#MIME_TYPE}. + * @param dateModified The date modified time of the media store media. Ok to default to 0. See + * {@link android.provider.MediaStore.Images.ImageColumns#DATE_MODIFIED} or + * {@link android.provider.MediaStore.Video.VideoColumns#DATE_MODIFIED}. + * @param orientation The orientation of the media store media. Ok to default to 0. See + * {@link android.provider.MediaStore.Images.ImageColumns#ORIENTATION}. + */ + public MediaStoreSignature(String mimeType, long dateModified, int orientation) { + this.mimeType = mimeType; + this.dateModified = dateModified; + this.orientation = orientation; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MediaStoreSignature that = (MediaStoreSignature) o; + + if (dateModified != that.dateModified) { + return false; + } + if (orientation != that.orientation) { + return false; + } + if (mimeType != null ? !mimeType.equals(that.mimeType) : that.mimeType != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = mimeType != null ? mimeType.hashCode() : 0; + result = 31 * result + (int) (dateModified ^ (dateModified >>> 32)); + result = 31 * result + orientation; + return result; + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + byte[] data = ByteBuffer.allocate(12) + .putLong(dateModified) + .putInt(orientation) + .array(); + messageDigest.update(data); + messageDigest.update(mimeType.getBytes(STRING_CHARSET_NAME)); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/signature/StringSignature.java b/core/src/main/java/com/example/bumptech/glide/signature/StringSignature.java new file mode 100755 index 0000000..6155b28 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/signature/StringSignature.java @@ -0,0 +1,52 @@ +package com.example.bumptech.glide.signature; + + +import com.example.bumptech.glide.load.Key; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; + +/** + * A unique Signature that wraps a String. + */ +public class StringSignature implements Key { + private final String signature; + + public StringSignature(String signature) { + if (signature == null) { + throw new NullPointerException("Signature cannot be null!"); + } + this.signature = signature; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StringSignature that = (StringSignature) o; + + return signature.equals(that.signature); + } + + @Override + public int hashCode() { + return signature.hashCode(); + } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) throws UnsupportedEncodingException { + messageDigest.update(signature.getBytes(STRING_CHARSET_NAME)); + } + + @Override + public String toString() { + return "StringSignature{" + + "signature='" + signature + '\'' + + '}'; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/ByteArrayPool.java b/core/src/main/java/com/example/bumptech/glide/util/ByteArrayPool.java new file mode 100755 index 0000000..70b15ad --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/ByteArrayPool.java @@ -0,0 +1,77 @@ +package com.example.bumptech.glide.util; + +import android.util.Log; + +import java.util.Queue; + +/** + * A pool for reusing byte arrays that produces and contains byte arrays of a fixed size. + */ +public final class ByteArrayPool { + private static final String TAG = "ByteArrayPool"; + // 64 KB. + private static final int TEMP_BYTES_SIZE = 64 * 1024; + // 512 KB. + private static final int MAX_SIZE = 2 * 1048 * 1024; + private static final int MAX_BYTE_ARRAY_COUNT = MAX_SIZE / TEMP_BYTES_SIZE; + + private final Queue tempQueue = Util.createQueue(0); + private static final ByteArrayPool BYTE_ARRAY_POOL = new ByteArrayPool(); + + /** + * Returns a constant singleton byte array pool. + */ + public static ByteArrayPool get() { + return BYTE_ARRAY_POOL; + } + + private ByteArrayPool() { } + + /** + * Removes all byte arrays from the pool. + */ + public void clear() { + synchronized (tempQueue) { + tempQueue.clear(); + } + } + + /** + * Returns a byte array by retrieving one from the pool if the pool is non empty or otherwise by creating a new + * byte array. + */ + public byte[] getBytes() { + byte[] result; + synchronized (tempQueue) { + result = tempQueue.poll(); + } + if (result == null) { + result = new byte[TEMP_BYTES_SIZE]; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Created temp bytes"); + } + } + return result; + } + + /** + * Adds the given byte array to the pool if it is the correct size and the pool is not full and returns true if + * the byte array was added and false otherwise. + * + * @param bytes The bytes to try to add to the pool. + */ + public boolean releaseBytes(byte[] bytes) { + if (bytes.length != TEMP_BYTES_SIZE) { + return false; + } + + boolean accepted = false; + synchronized (tempQueue) { + if (tempQueue.size() < MAX_BYTE_ARRAY_COUNT) { + accepted = true; + tempQueue.offer(bytes); + } + } + return accepted; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/ContentLengthInputStream.java b/core/src/main/java/com/example/bumptech/glide/util/ContentLengthInputStream.java new file mode 100755 index 0000000..c3baf0c --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/ContentLengthInputStream.java @@ -0,0 +1,78 @@ +package com.example.bumptech.glide.util; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Uses the content length as the basis for the return value of {@link #available()} and verifies + * that at least content length bytes are returned from the various read methods. + */ +public final class ContentLengthInputStream extends FilterInputStream { + private static final String TAG = "ContentLengthStream"; + private static final int UNKNOWN = -1; + + private final long contentLength; + private int readSoFar; + + public static InputStream obtain(InputStream other, String contentLengthHeader) { + return obtain(other, parseContentLength(contentLengthHeader)); + } + + public static InputStream obtain(InputStream other, long contentLength) { + return new ContentLengthInputStream(other, contentLength); + } + + private static int parseContentLength(String contentLengthHeader) { + int result = UNKNOWN; + if (!TextUtils.isEmpty(contentLengthHeader)) { + try { + result = Integer.parseInt(contentLengthHeader); + } catch (NumberFormatException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "failed to parse content length header: " + contentLengthHeader, e); + } + } + } + return result; + } + + ContentLengthInputStream(InputStream in, long contentLength) { + super(in); + this.contentLength = contentLength; + } + + @Override + public synchronized int available() throws IOException { + return (int) Math.max(contentLength - readSoFar, in.available()); + } + + @Override + public synchronized int read() throws IOException { + return checkReadSoFarOrThrow(super.read()); + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0 /*byteOffset*/, buffer.length /*byteCount*/); + } + + @Override + public synchronized int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + return checkReadSoFarOrThrow(super.read(buffer, byteOffset, byteCount)); + } + + private int checkReadSoFarOrThrow(int read) throws IOException { + if (read >= 0) { + readSoFar += read; + } else if (contentLength - readSoFar > 0) { + throw new IOException("Failed to read all expected data" + + ", expected: " + contentLength + + ", but read: " + readSoFar); + } + return read; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/ExceptionCatchingInputStream.java b/core/src/main/java/com/example/bumptech/glide/util/ExceptionCatchingInputStream.java new file mode 100755 index 0000000..17a9071 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/ExceptionCatchingInputStream.java @@ -0,0 +1,132 @@ +package com.example.bumptech.glide.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Queue; + +/** + * An {@link InputStream} that catches {@link IOException}s during read and skip calls and stores them + * so they can later be handled or thrown. This class is a workaround for a framework issue where exceptions during + * reads while decoding bitmaps in {@link android.graphics.BitmapFactory} can return partially decoded bitmaps. + * + * See https://github.com/bumptech/glide/issues/126. + */ +public class ExceptionCatchingInputStream extends InputStream { + + private static final Queue QUEUE = Util.createQueue(0); + + private InputStream wrapped; + private IOException exception; + + public static ExceptionCatchingInputStream obtain(InputStream toWrap) { + ExceptionCatchingInputStream result; + synchronized (QUEUE) { + result = QUEUE.poll(); + } + if (result == null) { + result = new ExceptionCatchingInputStream(); + } + result.setInputStream(toWrap); + return result; + } + + // Exposed for testing. + static void clearQueue() { + while (!QUEUE.isEmpty()) { + QUEUE.remove(); + } + } + + ExceptionCatchingInputStream() { + // Do nothing. + } + + void setInputStream(InputStream toWrap) { + wrapped = toWrap; + } + + @Override + public int available() throws IOException { + return wrapped.available(); + } + + @Override + public void close() throws IOException { + wrapped.close(); + } + + @Override + public void mark(int readlimit) { + wrapped.mark(readlimit); + } + + @Override + public boolean markSupported() { + return wrapped.markSupported(); + } + + @Override + public int read(byte[] buffer) throws IOException { + int read; + try { + read = wrapped.read(buffer); + } catch (IOException e) { + exception = e; + read = -1; + } + return read; + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + int read; + try { + read = wrapped.read(buffer, byteOffset, byteCount); + } catch (IOException e) { + exception = e; + read = -1; + } + return read; + } + + @Override + public synchronized void reset() throws IOException { + wrapped.reset(); + } + + @Override + public long skip(long byteCount) throws IOException { + long skipped; + try { + skipped = wrapped.skip(byteCount); + } catch (IOException e) { + exception = e; + skipped = 0; + } + return skipped; + } + + @Override + public int read() throws IOException { + int result; + try { + result = wrapped.read(); + } catch (IOException e) { + exception = e; + result = -1; + } + return result; + } + + public IOException getException() { + return exception; + } + + public void release() { + exception = null; + wrapped = null; + synchronized (QUEUE) { + QUEUE.offer(this); + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/FixedPreloadSizeProvider.java b/core/src/main/java/com/example/bumptech/glide/util/FixedPreloadSizeProvider.java new file mode 100755 index 0000000..e91e61f --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/FixedPreloadSizeProvider.java @@ -0,0 +1,31 @@ +package com.example.bumptech.glide.util; + + +import com.example.bumptech.glide.ListPreloader; + +import java.util.Arrays; + +/** + * A {@link ListPreloader.PreloadSizeProvider} with a fixed width and height. + * + * @param The type of the model the size should be provided for. + */ +public class FixedPreloadSizeProvider implements ListPreloader.PreloadSizeProvider { + + private final int[] size; + + /** + * Constructor for a PreloadSizeProvider with a fixed size. + * + * @param width The width of the preload size in pixels. + * @param height The height of the preload size in pixels. + */ + public FixedPreloadSizeProvider(int width, int height) { + this.size = new int[]{width, height}; + } + + @Override + public int[] getPreloadSize(T item, int adapterPosition, int itemPosition) { + return Arrays.copyOf(this.size, this.size.length); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/LogTime.java b/core/src/main/java/com/example/bumptech/glide/util/LogTime.java new file mode 100755 index 0000000..276d8d7 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/LogTime.java @@ -0,0 +1,39 @@ +package com.example.bumptech.glide.util; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.SystemClock; + +/** + * A class for logging elapsed real time in millis. + */ +public final class LogTime { + private static final double MILLIS_MULTIPLIER = + Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT ? 1d / Math.pow(10, 6) : 1d; + + private LogTime() { + // Utility class. + } + + /** + * Returns the current time in either millis or nanos depending on the api level to be used with + * {@link #getElapsedMillis(long)}. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static long getLogTime() { + if (Build.VERSION_CODES.JELLY_BEAN_MR1 <= Build.VERSION.SDK_INT) { + return SystemClock.elapsedRealtimeNanos(); + } else { + return System.currentTimeMillis(); + } + } + + /** + * Returns the time elapsed since the given logTime in millis. + * + * @param logTime The start time of the event. + */ + public static double getElapsedMillis(long logTime) { + return (getLogTime() - logTime) * MILLIS_MULTIPLIER; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/LruCache.java b/core/src/main/java/com/example/bumptech/glide/util/LruCache.java new file mode 100755 index 0000000..805e127 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/LruCache.java @@ -0,0 +1,169 @@ +package com.example.bumptech.glide.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A general purpose size limited cache that evicts items using an LRU algorithm. By default every item is assumed to + * have a size of one. Subclasses can override {@link #getSize(Object)}} to change the size on a per item basis. + * + * @param The type of the keys. + * @param The type of the values. + */ +public class LruCache { + private final LinkedHashMap cache = new LinkedHashMap(100, 0.75f, true); + private int maxSize; + private final int initialMaxSize; + private int currentSize = 0; + + /** + * Constructor for LruCache. + * + * @param size The maximum size of the cache, the units must match the units used in {@link #getSize(Object)}. + */ + public LruCache(int size) { + this.initialMaxSize = size; + this.maxSize = size; + } + + /** + * Sets a size multiplier that will be applied to the size provided in the constructor to set the new size of the + * cache. If the new size is less than the current size, entries will be evicted until the current size is less + * than or equal to the new size. + * + * @param multiplier The multiplier to apply. + */ + public void setSizeMultiplier(float multiplier) { + if (multiplier < 0) { + throw new IllegalArgumentException("Multiplier must be >= 0"); + } + maxSize = Math.round(initialMaxSize * multiplier); + evict(); + } + + /** + * Returns the size of a given item, defaulting to one. The units must match those used in the size passed in to the + * constructor. Subclasses can override this method to return sizes in various units, usually bytes. + * + * @param item The item to get the size of. + */ + protected int getSize(Y item) { + return 1; + } + + /** + * A callback called whenever an item is evicted from the cache. Subclasses can override. + * + * @param key The key of the evicted item. + * @param item The evicted item. + */ + protected void onItemEvicted(T key, Y item) { + // optional override + } + + /** + * Returns the current maximum size of the cache in bytes. + */ + public int getMaxSize() { + return maxSize; + } + + /** + * Returns the sum of the sizes of all items in the cache. + */ + public int getCurrentSize() { + return currentSize; + } + + /** + * Returns true if there is a value for the given key in the cache. + * + * @param key The key to check. + */ + + public boolean contains(T key) { + return cache.containsKey(key); + } + + /** + * Returns the item in the cache for the given key or null if no such item exists. + * + * @param key The key to check. + */ + public Y get(T key) { + return cache.get(key); + } + + /** + * Adds the given item to the cache with the given key and returns any previous entry for the given key that may + * have already been in the cache. + * + *

+ * If the size of the item is larger than the total cache size, the item will not be added to the cache and + * instead {@link #onItemEvicted(Object, Object)} will be called synchronously with the given key and item. + *

+ * + * @param key The key to add the item at. + * @param item The item to add. + */ + public Y put(T key, Y item) { + final int itemSize = getSize(item); + if (itemSize >= maxSize) { + onItemEvicted(key, item); + return null; + } + + final Y result = cache.put(key, item); + if (item != null) { + currentSize += getSize(item); + } + if (result != null) { + // TODO: should we call onItemEvicted here? + currentSize -= getSize(result); + } + evict(); + + return result; + } + + /** + * Removes the item at the given key and returns the removed item if present, and null otherwise. + * + * @param key The key to remove the item at. + */ + public Y remove(T key) { + final Y value = cache.remove(key); + if (value != null) { + currentSize -= getSize(value); + } + return value; + } + + /** + * Clears all items in the cache. + */ + public void clearMemory() { + trimToSize(0); + } + + /** + * Removes the least recently used items from the cache until the current size is less than the given size. + * + * @param size The size the cache should be less than. + */ + protected void trimToSize(int size) { + Map.Entry last; + while (currentSize > size) { + last = cache.entrySet().iterator().next(); + final Y toRemove = last.getValue(); + currentSize -= getSize(toRemove); + final T key = last.getKey(); + cache.remove(key); + onItemEvicted(key, toRemove); + } + } + + private void evict() { + trimToSize(maxSize); + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/MarkEnforcingInputStream.java b/core/src/main/java/com/example/bumptech/glide/util/MarkEnforcingInputStream.java new file mode 100755 index 0000000..e3dba9a --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/MarkEnforcingInputStream.java @@ -0,0 +1,87 @@ +package com.example.bumptech.glide.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Prevents {@link InputStream InputStreams} from overflowing their buffer by reading data past their read limit. + */ +public class MarkEnforcingInputStream extends FilterInputStream { + private static final int UNSET = Integer.MIN_VALUE; + private static final int END_OF_STREAM = -1; + + private int availableBytes = UNSET; + + public MarkEnforcingInputStream(InputStream in) { + super(in); + } + + @Override + public void mark(int readlimit) { + super.mark(readlimit); + availableBytes = readlimit; + } + + @Override + public int read() throws IOException { + if (getBytesToRead(1) == END_OF_STREAM) { + return END_OF_STREAM; + } + + int result = super.read(); + updateAvailableBytesAfterRead(1 /* bytesRead */); + return result; + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + int toRead = (int) getBytesToRead(byteCount); + if (toRead == END_OF_STREAM) { + return END_OF_STREAM; + } + + int read = super.read(buffer, byteOffset, toRead); + updateAvailableBytesAfterRead(read); + return read; + } + + @Override + public void reset() throws IOException { + super.reset(); + availableBytes = UNSET; + } + + @Override + public long skip(long byteCount) throws IOException { + long toSkip = getBytesToRead(byteCount); + if (toSkip == END_OF_STREAM) { + return END_OF_STREAM; + } + + long read = super.skip(toSkip); + updateAvailableBytesAfterRead(read); + return read; + } + + @Override + public int available() throws IOException { + return availableBytes == UNSET ? super.available() : Math.min(availableBytes, super.available()); + } + + private long getBytesToRead(long targetByteCount) { + if (availableBytes == 0) { + return END_OF_STREAM; + } else if (availableBytes != UNSET && targetByteCount > availableBytes) { + return availableBytes; + } else { + return targetByteCount; + } + } + + private void updateAvailableBytesAfterRead(long bytesRead) { + if (availableBytes != UNSET && bytesRead != END_OF_STREAM) { + availableBytes -= bytesRead; + } + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/MultiClassKey.java b/core/src/main/java/com/example/bumptech/glide/util/MultiClassKey.java new file mode 100755 index 0000000..aa4dd0e --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/MultiClassKey.java @@ -0,0 +1,58 @@ +package com.example.bumptech.glide.util; + +/** + * A key of two {@link Class}es to be used in hashed collections. + */ +public class MultiClassKey { + private Class first; + private Class second; + + public MultiClassKey() { + // leave them null + } + + public MultiClassKey(Class first, Class second) { + set(first, second); + } + + public void set(Class first, Class second) { + this.first = first; + this.second = second; + } + + @Override + public String toString() { + return "MultiClassKey{" + + "first=" + first + + ", second=" + second + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MultiClassKey that = (MultiClassKey) o; + + if (!first.equals(that.first)) { + return false; + } + if (!second.equals(that.second)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = first.hashCode(); + result = 31 * result + second.hashCode(); + return result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/Util.java b/core/src/main/java/com/example/bumptech/glide/util/Util.java new file mode 100755 index 0000000..ee0b4cb --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/Util.java @@ -0,0 +1,185 @@ +package com.example.bumptech.glide.util; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Looper; + + +import com.example.bumptech.glide.request.target.Target; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Queue; + +/** + * A collection of assorted utility classes. + */ +public final class Util { + private static final char[] HEX_CHAR_ARRAY = "0123456789abcdef".toCharArray(); + // 32 bytes from sha-256 -> 64 hex chars. + private static final char[] SHA_256_CHARS = new char[64]; + // 20 bytes from sha-1 -> 40 chars. + private static final char[] SHA_1_CHARS = new char[40]; + + private Util() { + // Utility class. + } + + /** + * Returns the hex string of the given byte array representing a SHA256 hash. + */ + public static String sha256BytesToHex(byte[] bytes) { + synchronized (SHA_256_CHARS) { + return bytesToHex(bytes, SHA_256_CHARS); + } + } + + /** + * Returns the hex string of the given byte array representing a SHA1 hash. + */ + public static String sha1BytesToHex(byte[] bytes) { + synchronized (SHA_1_CHARS) { + return bytesToHex(bytes, SHA_1_CHARS); + } + } + + // Taken from: + // http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java/9655275#9655275 + private static String bytesToHex(byte[] bytes, char[] hexChars) { + int v; + for (int j = 0; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Returns the allocated byte size of the given bitmap. + * + * @see #getBitmapByteSize(Bitmap) + * + * @deprecated Use {@link #getBitmapByteSize(Bitmap)} instead. Scheduled to be removed in Glide + * 4.0. + */ + @Deprecated + public static int getSize(Bitmap bitmap) { + return getBitmapByteSize(bitmap); + } + + /** + * Returns the in memory size of the given {@link Bitmap} in bytes. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + public static int getBitmapByteSize(Bitmap bitmap) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // Workaround for KitKat initial release NPE in Bitmap, fixed in MR1. See issue #148. + try { + return bitmap.getAllocationByteCount(); + } catch (NullPointerException e) { + // Do nothing. + } + } + return bitmap.getHeight() * bitmap.getRowBytes(); + } + + /** + * Returns the in memory size of {@link Bitmap} with the given width, height, and + * {@link Bitmap.Config}. + */ + public static int getBitmapByteSize(int width, int height, Bitmap.Config config) { + return width * height * getBytesPerPixel(config); + } + + private static int getBytesPerPixel(Bitmap.Config config) { + // A bitmap by decoding a gif has null "config" in certain environments. + if (config == null) { + config = Bitmap.Config.ARGB_8888; + } + + int bytesPerPixel; + switch (config) { + case ALPHA_8: + bytesPerPixel = 1; + break; + case RGB_565: + case ARGB_4444: + bytesPerPixel = 2; + break; + case ARGB_8888: + default: + bytesPerPixel = 4; + } + return bytesPerPixel; + } + + /** + * Returns true if width and height are both > 0 and/or equal to {@link Target#SIZE_ORIGINAL}. + */ + public static boolean isValidDimensions(int width, int height) { + return isValidDimension(width) && isValidDimension(height); + } + + private static boolean isValidDimension(int dimen) { + return dimen > 0 || dimen == Target.SIZE_ORIGINAL; + } + + /** + * Throws an {@link IllegalArgumentException} if called on a thread other than the main thread. + */ + public static void assertMainThread() { + if (!isOnMainThread()) { + throw new IllegalArgumentException("You must call this method on the main thread"); + } + } + + /** + * Throws an {@link IllegalArgumentException} if called on the main thread. + */ + public static void assertBackgroundThread() { + if (!isOnBackgroundThread()) { + throw new IllegalArgumentException("YOu must call this method on a background thread"); + } + } + + /** + * Returns {@code true} if called on the main thread, {@code false} otherwise. + */ + public static boolean isOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + + /** + * Returns {@code true} if called on the main thread, {@code false} otherwise. + */ + public static boolean isOnBackgroundThread() { + return !isOnMainThread(); + } + + /** + * Returns a {@link Queue} of the given size using Glide's preferred implementation. + */ + public static Queue createQueue(int size) { + return new ArrayDeque(size); + } + + /** + * Returns a copy of the given list that is safe to iterate over and perform actions that may + * modify the original list. + * + *

See #303 and #375.

+ */ + public static List getSnapshot(Collection other) { + // toArray creates a new ArrayList internally and this way we can guarantee entries will not + // be null. See #322. + List result = new ArrayList(other.size()); + for (T item : other) { + result.add(item); + } + return result; + } +} diff --git a/core/src/main/java/com/example/bumptech/glide/util/ViewPreloadSizeProvider.java b/core/src/main/java/com/example/bumptech/glide/util/ViewPreloadSizeProvider.java new file mode 100755 index 0000000..5e21ae9 --- /dev/null +++ b/core/src/main/java/com/example/bumptech/glide/util/ViewPreloadSizeProvider.java @@ -0,0 +1,87 @@ +package com.example.bumptech.glide.util; + +import android.view.View; + +import com.example.bumptech.glide.ListPreloader; +import com.example.bumptech.glide.request.animation.GlideAnimation; +import com.example.bumptech.glide.request.target.SizeReadyCallback; +import com.example.bumptech.glide.request.target.ViewTarget; + +import java.util.Arrays; + +/** + * A {@link ListPreloader.PreloadSizeProvider} that will extract the preload size from a given + * {@link View}. + * + * @param The type of the model the size should be provided for. + */ +public class ViewPreloadSizeProvider implements ListPreloader.PreloadSizeProvider, SizeReadyCallback { + private int[] size; + // We need to keep a strong reference to the Target so that it isn't garbage collected due to a weak reference + // while we're waiting to get its size. + @SuppressWarnings("unused") + private SizeViewTarget viewTarget; + + /** + * Constructor that does nothing by default and requires users to call {@link #setView(View)} when a + * View is available to registerComponents the dimensions returned by this class. + */ + public ViewPreloadSizeProvider() { + // This constructor is intentionally empty. Nothing special is needed here. + } + + /** + * Constructor that will extract the preload size from a given {@link View}. + * + * @param view A not null View the size will be extracted from async using an {@link android.view.ViewTreeObserver + * .OnPreDrawListener} + */ + public ViewPreloadSizeProvider(View view) { + setView(view); + } + + @Override + public int[] getPreloadSize(T item, int adapterPosition, int itemPosition) { + if (size == null) { + return null; + } else { + return Arrays.copyOf(this.size, this.size.length); + } + } + + @Override + public void onSizeReady(int width, int height) { + this.size = new int[]{width, height}; + viewTarget = null; + } + + /** + * Sets the {@link View} the size will be extracted. + * + *

+ * Note - only the first call to this method will be obeyed, subsequent requests will be ignored. + *

+ * + * @param view A not null View the size will be extracted async with an {@link android.view.ViewTreeObserver + * .OnPreDrawListener} + */ + public void setView(View view) { + if (this.size != null || viewTarget != null) { + return; + } + this.viewTarget = new SizeViewTarget(view, this); + } + + private static final class SizeViewTarget extends ViewTarget { + + public SizeViewTarget(View view, SizeReadyCallback callback) { + super(view); + getSize(callback); + } + + @Override + public void onResourceReady(Object resource, GlideAnimation glideAnimation) { + // Do nothing + } + } +} diff --git a/core/src/main/java/com/example/core/extension/Drawable.kt b/core/src/main/java/com/example/core/extension/Drawable.kt new file mode 100644 index 0000000..019e057 --- /dev/null +++ b/core/src/main/java/com/example/core/extension/Drawable.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) guolin, Suzhou Quxiang Inc. Open source codes for study only. + * Do not use for commercial purpose. + * + * 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.example.core.extension + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable + +/** + * Drawable扩展工具类。 + * + * @author guolin + * @since 2018/10/19 + */ + +fun Drawable.toBitmap(): Bitmap { + // 取 drawable 的长宽 + val w = intrinsicWidth + val h = intrinsicHeight + + // 取 drawable 的颜色格式 + val config = if (opacity != PixelFormat.OPAQUE) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 + // 建立对应 bitmap + val bitmap = Bitmap.createBitmap(w, h, config); + // 建立对应 bitmap 的画布 + val canvas = Canvas(bitmap); + setBounds(0, 0, w, h); + // 把 drawable 内容画到画布中 + draw(canvas); + return bitmap; +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/extension/Model.kt b/core/src/main/java/com/example/core/extension/Model.kt new file mode 100644 index 0000000..ac63b80 --- /dev/null +++ b/core/src/main/java/com/example/core/extension/Model.kt @@ -0,0 +1,55 @@ +package com.example.core.extension + +import com.example.core.GifFun +import com.example.core.model.Model +import kotlin.concurrent.thread + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 下午4:01 + * Describe: + */ +/** + * 查询Model并回调集合中第一条符合给定参数条件元素的下标,如未查到则回调-1。 + */ +fun findModelIndex(models: List?, modelId: Long, action: (index: Int) -> Unit) { + thread { + var index = -1 + if (models != null && !models.isEmpty()) { + for (i in models.indices) { + val model = models[i] + if (model.modelId == modelId) { + index = i + break + } + } + } + GifFun.getHandler().post { + action(index) + } + } +} + +/** + * 查询Model并回调集合中第一条符合给定参数条件元素的下标,如未查到则不进行回调。 + */ +fun searchModelIndex(models: List?, modelId: Long, action: (index: Int) -> Unit) { + thread { + var index = -1 + if (models != null && !models.isEmpty()) { + for (i in models.indices) { + val model = models[i] + if (model.modelId == modelId) { + index = i + break + } + } + } + if (index != -1) { + GifFun.getHandler().post { + action(index) + } + } + } +} + diff --git a/core/src/main/java/com/example/core/model/HotFeed.kt b/core/src/main/java/com/example/core/model/HotFeed.kt new file mode 100644 index 0000000..c3284a9 --- /dev/null +++ b/core/src/main/java/com/example/core/model/HotFeed.kt @@ -0,0 +1,14 @@ +package com.example.core.model + +import com.google.gson.annotations.SerializedName + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 上午11:33 + * Describe: + */ +class HotFeed :WaterFallFeed(){ + + @SerializedName("comments_count") + var commentsCount:Int=0 +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/util/GlobalUtil.kt b/core/src/main/java/com/example/core/util/GlobalUtil.kt index 0b5476a..9cbb9cc 100644 --- a/core/src/main/java/com/example/core/util/GlobalUtil.kt +++ b/core/src/main/java/com/example/core/util/GlobalUtil.kt @@ -69,4 +69,27 @@ object GlobalUtil { fun getResponseClue(status:Int,msg:String):String{ return "code: $status , msg: $msg" } + + // 获取转换之后的数字显示,如123456会被转换成12.3万。 + fun getConvertedNumber(number:Int)=when{ + number<10000->number.toString() + number<10000->{ + var converted=String.format(Locale.ENGLISH,"%.1f",number/10000.0) + if(converted.endsWith(".0")){ + converted=converted.replace(".0","") + } + converted+"万" + } + number<100100100->{ + val converted = number / 10000 + converted.toString() + "万" + } + else->{ + var converted = String.format(Locale.ENGLISH, "%.1f", number / 100_000_000.0) + if (converted.endsWith(".00")) { + converted = converted.replace(".00", "") + } + converted + "亿" + } + } } \ No newline at end of file diff --git a/core/src/main/java/jp/wasbeef/glide/transformations/BlurTransformation.java b/core/src/main/java/jp/wasbeef/glide/transformations/BlurTransformation.java new file mode 100755 index 0000000..151a978 --- /dev/null +++ b/core/src/main/java/jp/wasbeef/glide/transformations/BlurTransformation.java @@ -0,0 +1,110 @@ +package jp.wasbeef.glide.transformations; + +/** + * 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. + */ + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.renderscript.RSRuntimeException; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; + +import jp.wasbeef.glide.transformations.internal.FastBlur; +import jp.wasbeef.glide.transformations.internal.RSBlur; + + +public class BlurTransformation implements Transformation { + + private static int MAX_RADIUS = 25; + private static int DEFAULT_DOWN_SAMPLING = 1; + + private Context mContext; + private BitmapPool mBitmapPool; + + private int mRadius; + private int mSampling; + + public BlurTransformation(Context context) { + this(context, Glide.get(context).getBitmapPool(), MAX_RADIUS, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, BitmapPool pool) { + this(context, pool, MAX_RADIUS, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, BitmapPool pool, int radius) { + this(context, pool, radius, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, int radius) { + this(context, Glide.get(context).getBitmapPool(), radius, DEFAULT_DOWN_SAMPLING); + } + + public BlurTransformation(Context context, int radius, int sampling) { + this(context, Glide.get(context).getBitmapPool(), radius, sampling); + } + + public BlurTransformation(Context context, BitmapPool pool, int radius, int sampling) { + mContext = context.getApplicationContext(); + mBitmapPool = pool; + mRadius = radius; + mSampling = sampling; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + Bitmap source = resource.get(); + + int width = source.getWidth(); + int height = source.getHeight(); + int scaledWidth = width / mSampling; + int scaledHeight = height / mSampling; + + Bitmap bitmap = mBitmapPool.get(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + canvas.scale(1 / (float) mSampling, 1 / (float) mSampling); + Paint paint = new Paint(); + paint.setFlags(Paint.FILTER_BITMAP_FLAG); + canvas.drawBitmap(source, 0, 0, paint); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + try { + bitmap = RSBlur.blur(mContext, bitmap, mRadius); + } catch (RSRuntimeException e) { + bitmap = FastBlur.blur(bitmap, mRadius, true); + } + } else { + bitmap = FastBlur.blur(bitmap, mRadius, true); + } + + return BitmapResource.obtain(bitmap, mBitmapPool); + } + + @Override public String getId() { + return "BlurTransformation(radius=" + mRadius + ", sampling=" + mSampling + ")"; + } +} diff --git a/core/src/main/java/jp/wasbeef/glide/transformations/CropCircleTransformation.java b/core/src/main/java/jp/wasbeef/glide/transformations/CropCircleTransformation.java new file mode 100755 index 0000000..0a9c29f --- /dev/null +++ b/core/src/main/java/jp/wasbeef/glide/transformations/CropCircleTransformation.java @@ -0,0 +1,80 @@ +package jp.wasbeef.glide.transformations; + +/** + * 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. + */ + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; + +import com.example.bumptech.glide.Glide; +import com.example.bumptech.glide.load.Transformation; +import com.example.bumptech.glide.load.engine.Resource; +import com.example.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.example.bumptech.glide.load.resource.bitmap.BitmapResource; + + +public class CropCircleTransformation implements Transformation { + + private BitmapPool mBitmapPool; + + public CropCircleTransformation(Context context) { + this(Glide.get(context).getBitmapPool()); + } + + public CropCircleTransformation(BitmapPool pool) { + this.mBitmapPool = pool; + } + + @Override + public Resource transform(Resource resource, int outWidth, int outHeight) { + Bitmap source = resource.get(); + int size = Math.min(source.getWidth(), source.getHeight()); + + int width = (source.getWidth() - size) / 2; + int height = (source.getHeight() - size) / 2; + + Bitmap bitmap = mBitmapPool.get(size, size, Bitmap.Config.ARGB_8888); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + BitmapShader shader = + new BitmapShader(source, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); + if (width != 0 || height != 0) { + // source isn't square, move viewport to center + Matrix matrix = new Matrix(); + matrix.setTranslate(-width, -height); + shader.setLocalMatrix(matrix); + } + paint.setShader(shader); + paint.setAntiAlias(true); + + float r = size / 2f; + canvas.drawCircle(r, r, r, paint); + + return BitmapResource.obtain(bitmap, mBitmapPool); + } + + @Override public String getId() { + return "CropCircleTransformation()"; + } +} diff --git a/core/src/main/java/jp/wasbeef/glide/transformations/internal/FastBlur.java b/core/src/main/java/jp/wasbeef/glide/transformations/internal/FastBlur.java new file mode 100755 index 0000000..ef30847 --- /dev/null +++ b/core/src/main/java/jp/wasbeef/glide/transformations/internal/FastBlur.java @@ -0,0 +1,257 @@ +package jp.wasbeef.glide.transformations.internal; + +import android.graphics.Bitmap; + +/** + * 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. + */ + +public class FastBlur { + + public static Bitmap blur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) { + + // Stack Blur v1.0 from + // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html + // + // Java Author: Mario Klingemann + // http://incubator.quasimondo.com + // created Feburary 29, 2004 + // Android port : Yahel Bouaziz + // http://www.kayenko.com + // ported april 5th, 2012 + + // This is a compromise between Gaussian Blur and Box blur + // It creates much better looking blurs than Box Blur, but is + // 7x faster than my Gaussian Blur implementation. + // + // I called it Stack Blur because this describes best how this + // filter works internally: it creates a kind of moving stack + // of colors whilst scanning through the image. Thereby it + // just has to add one new block of color to the right side + // of the stack and remove the leftmost color. The remaining + // colors on the topmost layer of the stack are either added on + // or reduced by one, depending on if they are on the right or + // on the left side of the stack. + // + // If you are using this algorithm in your code please add + // the following line: + // + // Stack Blur Algorithm by Mario Klingemann + + Bitmap bitmap; + if (canReuseInBitmap) { + bitmap = sentBitmap; + } else { + bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); + } + + if (radius < 1) { + return (null); + } + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + int[] pix = new int[w * h]; + bitmap.getPixels(pix, 0, w, 0, 0, w, h); + + int wm = w - 1; + int hm = h - 1; + int wh = w * h; + int div = radius + radius + 1; + + int r[] = new int[wh]; + int g[] = new int[wh]; + int b[] = new int[wh]; + int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; + int vmin[] = new int[Math.max(w, h)]; + + int divsum = (div + 1) >> 1; + divsum *= divsum; + int dv[] = new int[256 * divsum]; + for (i = 0; i < 256 * divsum; i++) { + dv[i] = (i / divsum); + } + + yw = yi = 0; + + int[][] stack = new int[div][3]; + int stackpointer; + int stackstart; + int[] sir; + int rbs; + int r1 = radius + 1; + int routsum, goutsum, boutsum; + int rinsum, ginsum, binsum; + + for (y = 0; y < h; y++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + for (i = -radius; i <= radius; i++) { + p = pix[yi + Math.min(wm, Math.max(i, 0))]; + sir = stack[i + radius]; + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + rbs = r1 - Math.abs(i); + rsum += sir[0] * rbs; + gsum += sir[1] * rbs; + bsum += sir[2] * rbs; + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + } + stackpointer = radius; + + for (x = 0; x < w; x++) { + + r[yi] = dv[rsum]; + g[yi] = dv[gsum]; + b[yi] = dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm); + } + p = pix[yw + vmin[x]]; + + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[(stackpointer) % div]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi++; + } + yw += w; + } + for (x = 0; x < w; x++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + yp = -radius * w; + for (i = -radius; i <= radius; i++) { + yi = Math.max(0, yp) + x; + + sir = stack[i + radius]; + + sir[0] = r[yi]; + sir[1] = g[yi]; + sir[2] = b[yi]; + + rbs = r1 - Math.abs(i); + + rsum += r[yi] * rbs; + gsum += g[yi] * rbs; + bsum += b[yi] * rbs; + + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + + if (i < hm) { + yp += w; + } + } + yi = x; + stackpointer = radius; + for (y = 0; y < h; y++) { + // Preserve alpha channel: ( 0xff000000 & pix[yi] ) + pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w; + } + p = x + vmin[y]; + + sir[0] = r[p]; + sir[1] = g[p]; + sir[2] = b[p]; + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[stackpointer]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi += w; + } + } + + bitmap.setPixels(pix, 0, w, 0, 0, w, h); + + return (bitmap); + } +} \ No newline at end of file diff --git a/core/src/main/java/jp/wasbeef/glide/transformations/internal/RSBlur.java b/core/src/main/java/jp/wasbeef/glide/transformations/internal/RSBlur.java new file mode 100755 index 0000000..91e251b --- /dev/null +++ b/core/src/main/java/jp/wasbeef/glide/transformations/internal/RSBlur.java @@ -0,0 +1,66 @@ +package jp.wasbeef.glide.transformations.internal; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RSRuntimeException; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; + +/** + * 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. + */ + +public class RSBlur { + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + public static Bitmap blur(Context context, Bitmap bitmap, int radius) throws RSRuntimeException { + RenderScript rs = null; + Allocation input = null; + Allocation output = null; + ScriptIntrinsicBlur blur = null; + try { + rs = RenderScript.create(context); + rs.setMessageHandler(new RenderScript.RSMessageHandler()); + input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT); + output = Allocation.createTyped(rs, input.getType()); + blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + + blur.setInput(input); + blur.setRadius(radius); + blur.forEach(output); + output.copyTo(bitmap); + } finally { + if (rs != null) { + rs.destroy(); + } + if (input != null) { + input.destroy(); + } + if (output != null) { + output.destroy(); + } + if (blur != null) { + blur.destroy(); + } + } + + return bitmap; + } +} diff --git a/main/src/main/assets/litepal.xml b/main/src/main/assets/litepal.xml index 4ebdd85..43aee94 100644 --- a/main/src/main/assets/litepal.xml +++ b/main/src/main/assets/litepal.xml @@ -32,10 +32,10 @@ --> - - - - + + + + + + + + + + diff --git a/main/src/main/res/values-v21/transition_svg.xml b/main/src/main/res/values-v21/transition_svg.xml index bcb53cc..9c70813 100644 --- a/main/src/main/res/values-v21/transition_svg.xml +++ b/main/src/main/res/values-v21/transition_svg.xml @@ -8,4 +8,7 @@ transition_logo_splash + transition_feed_detail + transition_feed_detail_bg + transition_feed_detail_image_bg \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FetchWorldFeeds.kt b/network/src/main/java/com/quxianggif/network/model/FetchWorldFeeds.kt new file mode 100644 index 0000000..2cfb90f --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FetchWorldFeeds.kt @@ -0,0 +1,28 @@ +package com.quxianggif.network.model + +import com.example.core.model.WorldFeed +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.FetchWorldFeedsRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 下午3:30 + * Describe: 获取世界频道Feeds请求的实体类封装。 + */ +class FetchWorldFeeds :Response(){ + @SerializedName("data") + var feeds:List =ArrayList() + + companion object{ + fun getResponse(callback: Callback) { + FetchWorldFeedsRequest() + .listen(callback) + } + + fun getResponse(lastFeed: Long, callback: Callback) { + FetchWorldFeedsRequest() + .lastFeed(lastFeed) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/LikeFeed.kt b/network/src/main/java/com/quxianggif/network/model/LikeFeed.kt new file mode 100644 index 0000000..9910e09 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/LikeFeed.kt @@ -0,0 +1,19 @@ +package com.quxianggif.network.model + +import com.quxianggif.network.request.LikeFeedRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 下午2:09 + * Describe:Feed点赞请求的实体类封装 + */ +class LikeFeed :Response(){ + companion object{ + + fun getResponse(feedId:Long,callback:Callback?){ + LikeFeedRequest() + .feed(feedId) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FetchWorldFeedsRequest.kt b/network/src/main/java/com/quxianggif/network/request/FetchWorldFeedsRequest.kt new file mode 100644 index 0000000..13c17af --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FetchWorldFeedsRequest.kt @@ -0,0 +1,57 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FetchWorldFeeds +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 下午3:32 + * Describe:获取世界频道Feeds请求。对应服务器接口:/feeds/world + */ +class FetchWorldFeedsRequest :Request(){ + + private var lastFeed:Long=0 + + fun lastFeed(lastFeed:Long):FetchWorldFeedsRequest{ + this.lastFeed=lastFeed + return this + } + + override fun url(): String { + return URL + } + + override fun method(): Int { + return Request.GET + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FetchWorldFeeds::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + if(lastFeed>0){ + params[NetworkConst.LAST_FEED]=lastFeed.toString() + } + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.UID,NetworkConst.TOKEN) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/world" + } + +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/LikeFeedRequest.kt b/network/src/main/java/com/quxianggif/network/request/LikeFeedRequest.kt new file mode 100644 index 0000000..ac8d9ba --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/LikeFeedRequest.kt @@ -0,0 +1,54 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.LikeFeed +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/12 下午2:13 + * Describe:Feed点赞请求 + */ +class LikeFeedRequest :Request(){ + + private var feed:Long=0 + + fun feed(feed:Long):LikeFeedRequest{ + this.feed=feed + return this + } + + override fun url(): String { + return URL + } + + override fun method(): Int { + return Request.POST + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(LikeFeed::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + params[NetworkConst.FEED]=feed.toString() + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.FEED,NetworkConst.TOKEN) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/like" + } +} \ No newline at end of file From 2835336424c9bf0cb9fc5374cc8272eb2b92632c Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Fri, 14 Jun 2019 14:17:52 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=E7=83=AD=E9=97=A8=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/core/model/FollowingFeed.kt | 18 + .../java/com/example/core/model/RefFeed.kt | 8 + .../com/example/core/model/SimpleListFeed.kt | 16 + .../java/com/example/core/model/UserFeed.kt | 18 + main/src/main/assets/litepal.xml | 6 +- .../common/adapter/SimpleListFeedAdapter.kt | 415 ++++++++++++++++++ .../adapter/WaterFallFeedAdapter.kt | 301 +++++++------ .../example/main/event/DeleteCommentEvent.kt | 13 + .../example/main/event/PostCommentEvent.kt | 12 + .../main/event/RefreshFollowingFeedsEvent.kt | 8 + .../feeds/adapter/FollowingFeedAdapter.kt | 58 +++ .../main/feeds/adapter/HotFeedAdapter.kt | 3 +- .../main/feeds/adapter/WorldFeedAdapter.kt | 5 +- .../main/feeds/ui/FeedDetailActivity.kt | 2 +- .../main/feeds/ui/FollowingFeedsFragment.kt | 260 +++++++++++ .../example/main/feeds/ui/HotFeedsFragment.kt | 168 ++++++- .../com/example/main/feeds/ui/MainActivity.kt | 2 +- .../main/feeds/view/SpaceItemDecoration.kt | 2 +- .../main/user/adapter/UserFeedAdapter.kt | 33 ++ .../main/user/ui/UserHomePageActivity.kt | 70 +++ .../java/com/example/main/util/DateUtil.kt | 111 +++++ .../main/res/drawable-v21/heart_anim_20dp.xml | 43 ++ .../drawable-v21/heart_anim_reverse_20dp.xml | 44 ++ .../res/drawable-v21/heart_empty_20dp.xml | 14 + .../main/res/drawable-v21/heart_fill_20dp.xml | 14 + .../main/res/drawable-v21/ic_heart_20dp.xml | 30 ++ .../res/drawable-v21/ic_heart_empty_20dp.xml | 30 ++ .../res/drawable-v21/ic_heart_full_20dp.xml | 30 ++ .../main/res/drawable-v22/ic_heart_20dp.xml | 42 ++ .../main/res/layout-v21/user_feed_item.xml | 226 ++++++++++ .../main/res/layout-v21/user_refeed_item.xml | 246 +++++++++++ main/src/main/res/layout/user_feed_item.xml | 232 ++++++++++ main/src/main/res/layout/user_refeed_item.xml | 253 +++++++++++ main/src/main/res/values/styles.xml | 7 + .../quxianggif/network/model/DeleteFeed.kt | 19 + .../network/model/FetchFollowingFeeds.kt | 30 ++ .../quxianggif/network/model/FetchHotFeeds.kt | 30 ++ .../network/request/DeleteFeedRequest.kt | 54 +++ .../request/FetchFollowingFeedsRequest.kt | 57 +++ .../network/request/FetchHotFeedsRequest.kt | 55 +++ 40 files changed, 2836 insertions(+), 149 deletions(-) create mode 100644 core/src/main/java/com/example/core/model/FollowingFeed.kt create mode 100644 core/src/main/java/com/example/core/model/RefFeed.kt create mode 100644 core/src/main/java/com/example/core/model/SimpleListFeed.kt create mode 100644 core/src/main/java/com/example/core/model/UserFeed.kt create mode 100644 main/src/main/java/com/example/main/common/adapter/SimpleListFeedAdapter.kt rename main/src/main/java/com/example/main/{feeds => common}/adapter/WaterFallFeedAdapter.kt (52%) create mode 100644 main/src/main/java/com/example/main/event/DeleteCommentEvent.kt create mode 100644 main/src/main/java/com/example/main/event/PostCommentEvent.kt create mode 100644 main/src/main/java/com/example/main/event/RefreshFollowingFeedsEvent.kt create mode 100644 main/src/main/java/com/example/main/feeds/adapter/FollowingFeedAdapter.kt create mode 100644 main/src/main/java/com/example/main/feeds/ui/FollowingFeedsFragment.kt create mode 100644 main/src/main/java/com/example/main/user/adapter/UserFeedAdapter.kt create mode 100644 main/src/main/java/com/example/main/util/DateUtil.kt create mode 100755 main/src/main/res/drawable-v21/heart_anim_20dp.xml create mode 100755 main/src/main/res/drawable-v21/heart_anim_reverse_20dp.xml create mode 100644 main/src/main/res/drawable-v21/heart_empty_20dp.xml create mode 100644 main/src/main/res/drawable-v21/heart_fill_20dp.xml create mode 100755 main/src/main/res/drawable-v21/ic_heart_20dp.xml create mode 100755 main/src/main/res/drawable-v21/ic_heart_empty_20dp.xml create mode 100755 main/src/main/res/drawable-v21/ic_heart_full_20dp.xml create mode 100755 main/src/main/res/drawable-v22/ic_heart_20dp.xml create mode 100755 main/src/main/res/layout-v21/user_feed_item.xml create mode 100755 main/src/main/res/layout-v21/user_refeed_item.xml create mode 100755 main/src/main/res/layout/user_feed_item.xml create mode 100755 main/src/main/res/layout/user_refeed_item.xml create mode 100644 network/src/main/java/com/quxianggif/network/model/DeleteFeed.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/FetchFollowingFeeds.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/FetchHotFeeds.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/DeleteFeedRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/FetchFollowingFeedsRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/FetchHotFeedsRequest.kt diff --git a/core/src/main/java/com/example/core/model/FollowingFeed.kt b/core/src/main/java/com/example/core/model/FollowingFeed.kt new file mode 100644 index 0000000..9b3f0d8 --- /dev/null +++ b/core/src/main/java/com/example/core/model/FollowingFeed.kt @@ -0,0 +1,18 @@ +package com.example.core.model + +import com.google.gson.annotations.SerializedName + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午4:47 + * Describe:FollowingFeed的实体类,用于存储服务器返回的用户关注列表的Feed数据 + */ +class FollowingFeed :SimpleListFeed(){ + + @SerializedName("ref_feed") + var refFeed:RefFeed?=null + + override fun refFeed(): BaseFeed? { + return refFeed + } +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/model/RefFeed.kt b/core/src/main/java/com/example/core/model/RefFeed.kt new file mode 100644 index 0000000..a08adca --- /dev/null +++ b/core/src/main/java/com/example/core/model/RefFeed.kt @@ -0,0 +1,8 @@ +package com.example.core.model + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午4:51 + * Describe:转发引用Feed的实体类,用于存储转发Feed中引用的Feed对象。 + */ +class RefFeed :BaseFeed() \ No newline at end of file diff --git a/core/src/main/java/com/example/core/model/SimpleListFeed.kt b/core/src/main/java/com/example/core/model/SimpleListFeed.kt new file mode 100644 index 0000000..22249db --- /dev/null +++ b/core/src/main/java/com/example/core/model/SimpleListFeed.kt @@ -0,0 +1,16 @@ +package com.example.core.model + +import com.google.gson.annotations.SerializedName + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午4:48 + * Describe:SimpleListFeed的实体类,用于存储单列列表展示的Feed数据。 + */ +abstract class SimpleListFeed :BaseFeed(){ + + @SerializedName("feed_type") + var feedType=0 + + abstract fun refFeed():BaseFeed? +} \ No newline at end of file diff --git a/core/src/main/java/com/example/core/model/UserFeed.kt b/core/src/main/java/com/example/core/model/UserFeed.kt new file mode 100644 index 0000000..f96cca1 --- /dev/null +++ b/core/src/main/java/com/example/core/model/UserFeed.kt @@ -0,0 +1,18 @@ +package com.example.core.model + +import com.google.gson.annotations.SerializedName + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/14 上午10:34 + * Describe:UserFeed的实体类,用于存储服务器返回的用户个人主页列表的Feed数据。 + */ +class UserFeed :SimpleListFeed(){ + + @SerializedName("ref_feed") + var refFeed:RefFeed?=null + + override fun refFeed(): RefFeed? { + return refFeed + } +} \ No newline at end of file diff --git a/main/src/main/assets/litepal.xml b/main/src/main/assets/litepal.xml index 43aee94..df0ebb7 100644 --- a/main/src/main/assets/litepal.xml +++ b/main/src/main/assets/litepal.xml @@ -33,9 +33,9 @@ --> - - - + + + + + + + + + + + + + + + diff --git a/main/src/main/res/drawable-v21/heart_anim_reverse_20dp.xml b/main/src/main/res/drawable-v21/heart_anim_reverse_20dp.xml new file mode 100755 index 0000000..1fbf3ba --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_anim_reverse_20dp.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/main/src/main/res/drawable-v21/heart_empty_20dp.xml b/main/src/main/res/drawable-v21/heart_empty_20dp.xml new file mode 100644 index 0000000..b4393c9 --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_empty_20dp.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/heart_fill_20dp.xml b/main/src/main/res/drawable-v21/heart_fill_20dp.xml new file mode 100644 index 0000000..a0dc7c4 --- /dev/null +++ b/main/src/main/res/drawable-v21/heart_fill_20dp.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/drawable-v21/ic_heart_20dp.xml b/main/src/main/res/drawable-v21/ic_heart_20dp.xml new file mode 100755 index 0000000..63611d9 --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_20dp.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/main/src/main/res/drawable-v21/ic_heart_empty_20dp.xml b/main/src/main/res/drawable-v21/ic_heart_empty_20dp.xml new file mode 100755 index 0000000..5de5db8 --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_empty_20dp.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/main/src/main/res/drawable-v21/ic_heart_full_20dp.xml b/main/src/main/res/drawable-v21/ic_heart_full_20dp.xml new file mode 100755 index 0000000..604f2c4 --- /dev/null +++ b/main/src/main/res/drawable-v21/ic_heart_full_20dp.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/main/src/main/res/drawable-v22/ic_heart_20dp.xml b/main/src/main/res/drawable-v22/ic_heart_20dp.xml new file mode 100755 index 0000000..174fcc7 --- /dev/null +++ b/main/src/main/res/drawable-v22/ic_heart_20dp.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/main/src/main/res/layout-v21/user_feed_item.xml b/main/src/main/res/layout-v21/user_feed_item.xml new file mode 100755 index 0000000..640420e --- /dev/null +++ b/main/src/main/res/layout-v21/user_feed_item.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/layout-v21/user_refeed_item.xml b/main/src/main/res/layout-v21/user_refeed_item.xml new file mode 100755 index 0000000..c4a5d7e --- /dev/null +++ b/main/src/main/res/layout-v21/user_refeed_item.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/layout/user_feed_item.xml b/main/src/main/res/layout/user_feed_item.xml new file mode 100755 index 0000000..dde4b0d --- /dev/null +++ b/main/src/main/res/layout/user_feed_item.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/layout/user_refeed_item.xml b/main/src/main/res/layout/user_refeed_item.xml new file mode 100755 index 0000000..49c1ca9 --- /dev/null +++ b/main/src/main/res/layout/user_refeed_item.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/src/main/res/values/styles.xml b/main/src/main/res/values/styles.xml index 544f555..00f9326 100644 --- a/main/src/main/res/values/styles.xml +++ b/main/src/main/res/values/styles.xml @@ -41,4 +41,11 @@ @color/ripple_light @color/colorAccent + + \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/DeleteFeed.kt b/network/src/main/java/com/quxianggif/network/model/DeleteFeed.kt new file mode 100644 index 0000000..4fb6667 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/DeleteFeed.kt @@ -0,0 +1,19 @@ +package com.quxianggif.network.model + +import com.quxianggif.network.request.DeleteFeedRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午6:30 + * Describe:删除Feed请求的实体类封装。 + */ +class DeleteFeed :Response() { + + companion object{ + fun getResponse(feedId:Long,callback:Callback){ + DeleteFeedRequest() + .feed(feedId) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FetchFollowingFeeds.kt b/network/src/main/java/com/quxianggif/network/model/FetchFollowingFeeds.kt new file mode 100644 index 0000000..243b41a --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FetchFollowingFeeds.kt @@ -0,0 +1,30 @@ +package com.quxianggif.network.model + +import com.example.core.model.FollowingFeed +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.FetchFollowingFeedsRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午6:58 + * Describe:获取用户所关注的人所发Feeds请求的实体类封装。 + */ +class FetchFollowingFeeds :Response() { + + @SerializedName("data") + var feeds: List = ArrayList() + + companion object { + + fun getResponse(callback: Callback) { + FetchFollowingFeedsRequest() + .listen(callback) + } + + fun getResponse(lastFeed: Long, callback: Callback) { + FetchFollowingFeedsRequest() + .lastFeed(lastFeed) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FetchHotFeeds.kt b/network/src/main/java/com/quxianggif/network/model/FetchHotFeeds.kt new file mode 100644 index 0000000..e74e9de --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FetchHotFeeds.kt @@ -0,0 +1,30 @@ +package com.quxianggif.network.model + +import com.example.core.model.HotFeed +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.FetchHotFeedsRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/14 上午11:39 + * Describe:获取热门Feeds请求的实体类封装。 + */ +class FetchHotFeeds :Response(){ + + @SerializedName("data") + var feeds:List =ArrayList() + + companion object{ + + fun getResponse(callback: Callback){ + FetchHotFeedsRequest() + .listen(callback) + } + + fun getLoadingMoreResponse(callback: Callback) { + FetchHotFeedsRequest() + .isLoadingMore(true) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/DeleteFeedRequest.kt b/network/src/main/java/com/quxianggif/network/request/DeleteFeedRequest.kt new file mode 100644 index 0000000..dfaf20a --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/DeleteFeedRequest.kt @@ -0,0 +1,54 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.DeleteFeed +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午6:32 + * Describe:删除Feed请求。对应服务器接口:/feeds/delete + */ +class DeleteFeedRequest :Request(){ + + private var feed:Long=0 + + fun feed(feed:Long):DeleteFeedRequest{ + this.feed=feed + return this + } + + override fun method(): Int { + return POST + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(DeleteFeed::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if (buildAuthParams(params)) { + params[NetworkConst.FEED] = feed.toString() + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder, NetworkConst.TOKEN, NetworkConst.DEVICE_SERIAL, NetworkConst.FEED) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/delete" + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FetchFollowingFeedsRequest.kt b/network/src/main/java/com/quxianggif/network/request/FetchFollowingFeedsRequest.kt new file mode 100644 index 0000000..f2e06f4 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FetchFollowingFeedsRequest.kt @@ -0,0 +1,57 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FetchFollowingFeeds +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/13 下午7:00 + * Describe:获取用户所关注的人所发Feeds的请求。对应服务器接口:/feeds/followings + */ +class FetchFollowingFeedsRequest:Request(){ + + private var lastFeed:Long=0 + + fun lastFeed(lastFeed:Long):FetchFollowingFeedsRequest{ + this.lastFeed=lastFeed + return this + } + + override fun method(): Int { + return GET + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FetchFollowingFeeds::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + if(lastFeed>0){ + params[NetworkConst.LAST_FEED]=lastFeed.toString() + } + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.TOKEN,NetworkConst.UID, NetworkConst.DEVICE_SERIAL) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/followings" + } + +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FetchHotFeedsRequest.kt b/network/src/main/java/com/quxianggif/network/request/FetchHotFeedsRequest.kt new file mode 100644 index 0000000..3ea1ee6 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FetchHotFeedsRequest.kt @@ -0,0 +1,55 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FetchHotFeeds +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/6/14 上午11:41 + * Describe:获取热门Feeds请求。对应服务器接口:/feeds/hot + */ +class FetchHotFeedsRequest :Request(){ + + private var isLoadingMore=false + + fun isLoadingMore(isLoadingMore:Boolean):FetchHotFeedsRequest{ + this.isLoadingMore=isLoadingMore + return this + } + + override fun method(): Int { + return GET + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FetchHotFeeds::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + params[NetworkConst.LOADING_MORE]=isLoadingMore.toString() + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.TOKEN, NetworkConst.LOADING_MORE, NetworkConst.DEVICE_SERIAL) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/hot" + } + +} \ No newline at end of file From d5eeeaec36635994ad559377038860028cd1c4ea Mon Sep 17 00:00:00 2001 From: zhangmingzhu Date: Fri, 19 Jul 2019 15:49:02 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/core/extension/String.kt | 15 + main/src/main/AndroidManifest.xml | 27 + .../main/common/view/TransparentToolbar.kt | 28 + .../com/example/main/event/FollowUserEvent.kt | 21 + .../main/event/LoadOriginAvatarEvent.kt | 8 + .../com/example/main/report/ReportActivity.kt | 52 ++ .../java/com/example/main/ui/BaseActivity.kt | 23 +- .../main/user/adapter/UserFeedAdapter.kt | 57 +- .../main/user/ui/BrowserPhotoActivity.kt | 35 + .../main/user/ui/FollowshipActivity.kt | 38 + .../main/user/ui/UserHomePageActivity.kt | 682 +++++++++++++++++- .../drawable/user_home_page_bg_default.xml | 6 + .../src/main/res/layout-v21/hot_feed_item.xml | 2 +- .../res/layout/activity_user_home_page.xml | 219 ++++++ main/src/main/res/layout/hot_feed_item.xml | 2 +- main/src/main/res/layout/user_feed_item.xml | 2 +- main/src/main/res/layout/user_refeed_item.xml | 2 +- main/src/main/res/layout/world_feed_item.xml | 2 +- .../src/main/res/menu/menu_user_home_page.xml | 16 + main/src/main/res/values-v21/styles.xml | 4 + .../main/res/values-v21/transition_svg.xml | 36 +- .../network/model/FetchUserFeeds.kt | 56 ++ .../quxianggif/network/model/FollowUser.kt | 26 + .../quxianggif/network/model/UnfollowUser.kt | 20 + .../network/request/FetchUserFeedsRequest.kt | 68 ++ .../network/request/FollowUserRequest.kt | 64 ++ .../network/request/UnfollowUserRequest.kt | 54 ++ 27 files changed, 1542 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/com/example/core/extension/String.kt create mode 100644 main/src/main/java/com/example/main/common/view/TransparentToolbar.kt create mode 100644 main/src/main/java/com/example/main/event/FollowUserEvent.kt create mode 100644 main/src/main/java/com/example/main/event/LoadOriginAvatarEvent.kt create mode 100644 main/src/main/java/com/example/main/report/ReportActivity.kt create mode 100644 main/src/main/java/com/example/main/user/ui/BrowserPhotoActivity.kt create mode 100644 main/src/main/java/com/example/main/user/ui/FollowshipActivity.kt create mode 100644 main/src/main/res/drawable/user_home_page_bg_default.xml create mode 100644 main/src/main/res/layout/activity_user_home_page.xml create mode 100644 main/src/main/res/menu/menu_user_home_page.xml create mode 100644 network/src/main/java/com/quxianggif/network/model/FetchUserFeeds.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/FollowUser.kt create mode 100644 network/src/main/java/com/quxianggif/network/model/UnfollowUser.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/FetchUserFeedsRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/FollowUserRequest.kt create mode 100644 network/src/main/java/com/quxianggif/network/request/UnfollowUserRequest.kt diff --git a/core/src/main/java/com/example/core/extension/String.kt b/core/src/main/java/com/example/core/extension/String.kt new file mode 100644 index 0000000..bf8a951 --- /dev/null +++ b/core/src/main/java/com/example/core/extension/String.kt @@ -0,0 +1,15 @@ +package com.example.core.extension + +import android.text.TextUtils + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午3:36 + * Describe:字符串操作的扩展工具类。 + */ + +fun String.getNumbersFromString() = if(TextUtils.isEmpty(this)){ + "" +}else{ + replace("[^0-9]".toRegex(), "") +} \ No newline at end of file diff --git a/main/src/main/AndroidManifest.xml b/main/src/main/AndroidManifest.xml index a89faed..f884359 100644 --- a/main/src/main/AndroidManifest.xml +++ b/main/src/main/AndroidManifest.xml @@ -13,5 +13,32 @@ android:name=".feeds.ui.MainActivity" android:screenOrientation="portrait" android:theme="@style/GifFun.MainActivity"/> + + + + + + + + + + + + + diff --git a/main/src/main/java/com/example/main/common/view/TransparentToolbar.kt b/main/src/main/java/com/example/main/common/view/TransparentToolbar.kt new file mode 100644 index 0000000..4cb9348 --- /dev/null +++ b/main/src/main/java/com/example/main/common/view/TransparentToolbar.kt @@ -0,0 +1,28 @@ +package com.example.main.common.view + +import android.content.Context +import android.support.v7.widget.Toolbar +import android.util.AttributeSet +import android.view.MotionEvent + + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/18 下午5:25 + * Describe:用于需要透明化显示的Toolbar,使用这种Toolbar后,即使被Toolbar盖住的区域依然可以点击,点击了被Toolbar盖住的部分 + * 明明可以看到却无法点击的问题 + */ + +class TransparentToolbar : Toolbar { + + constructor(context:Context):super(context){} + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {} + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {} + + override fun onTouchEvent(ev: MotionEvent?): Boolean { + return false + } + +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/event/FollowUserEvent.kt b/main/src/main/java/com/example/main/event/FollowUserEvent.kt new file mode 100644 index 0000000..edc4dbb --- /dev/null +++ b/main/src/main/java/com/example/main/event/FollowUserEvent.kt @@ -0,0 +1,21 @@ +package com.example.main.event + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 上午11:42 + * Describe:关注和取消关注用户的事件消息 + * + */ +class FollowUserEvent :MessageEvent() { + + var userId:Long=0 + + var type: Int = 0 + + companion object { + + val FOLLOW_USER = 0 + + val UNFOLLOW_USER = 1 + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/event/LoadOriginAvatarEvent.kt b/main/src/main/java/com/example/main/event/LoadOriginAvatarEvent.kt new file mode 100644 index 0000000..b6686a4 --- /dev/null +++ b/main/src/main/java/com/example/main/event/LoadOriginAvatarEvent.kt @@ -0,0 +1,8 @@ +package com.example.main.event + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午3:38 + * Describe:浏览用户头像大图的事件。 + */ +class LoadOriginAvatarEvent :MessageEvent() \ No newline at end of file diff --git a/main/src/main/java/com/example/main/report/ReportActivity.kt b/main/src/main/java/com/example/main/report/ReportActivity.kt new file mode 100644 index 0000000..780234c --- /dev/null +++ b/main/src/main/java/com/example/main/report/ReportActivity.kt @@ -0,0 +1,52 @@ +package com.example.main.report + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.example.main.ui.BaseActivity + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午3:31 + * Describe:用户对Feed、评论、用户进行举报的Activity。 + */ +class ReportActivity :BaseActivity(){ + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + companion object{ + private const val INTENT_REPORT_TYPE = "intent_report_type" + private const val INTENT_FEED_ID = "intent_feed_id" + private const val INTENT_COMMENT_ID = "intent_comment_id" + private const val INTENT_USER_ID = "intent_user_id" + + private const val REPORT_FEED = 0 + + private const val REPORT_COMMENT = 1 + + private const val REPORT_USER = 2 + + fun actionReportFeed(context: Context, feedId: Long) { + val intent = Intent(context, ReportActivity::class.java) + intent.putExtra(INTENT_REPORT_TYPE, REPORT_FEED) + intent.putExtra(INTENT_FEED_ID, feedId) + context.startActivity(intent) + } + + fun actionReportComment(context: Context, commentId: Long) { + val intent = Intent(context, ReportActivity::class.java) + intent.putExtra(INTENT_REPORT_TYPE, REPORT_COMMENT) + intent.putExtra(INTENT_COMMENT_ID, commentId) + context.startActivity(intent) + } + + fun actionReportUser(context: Context, userId: Long) { + val intent = Intent(context, ReportActivity::class.java) + intent.putExtra(INTENT_REPORT_TYPE, REPORT_USER) + intent.putExtra(INTENT_USER_ID, userId) + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/ui/BaseActivity.kt b/main/src/main/java/com/example/main/ui/BaseActivity.kt index 6a362eb..90ed969 100644 --- a/main/src/main/java/com/example/main/ui/BaseActivity.kt +++ b/main/src/main/java/com/example/main/ui/BaseActivity.kt @@ -1,5 +1,6 @@ package com.example.main.ui +import android.annotation.SuppressLint import android.app.Activity import android.app.ProgressDialog import android.content.Context @@ -21,6 +22,7 @@ import android.widget.TextView import com.example.core.util.AndroidVersion import com.example.main.R import com.example.main.common.callback.PermissionListener +import com.example.main.common.callback.RequestLifecycle import com.example.main.event.ForceToLoginEvent import com.example.main.event.MessageEvent import com.example.main.login.ui.LoginActivity @@ -37,7 +39,8 @@ import java.lang.ref.WeakReference * Date: 2019/4/8 上午11:24 * Describe:Activity的基类 */ -open class BaseActivity :AppCompatActivity() { +@SuppressLint("Registered") +open class BaseActivity :AppCompatActivity(),RequestLifecycle { //判断当前Activity是否在前台 protected var isActive: Boolean = false @@ -243,6 +246,24 @@ open class BaseActivity :AppCompatActivity() { } } + @CallSuper + override fun startLoading() { + loading?.visibility = View.VISIBLE + hideBadNetworkView() + hideNoContentView() + hideLoadErrorView() + } + + @CallSuper + override fun loadFinished() { + loading?.visibility = View.GONE + } + + @CallSuper + override fun loadFailed(msg: String?) { + loading?.visibility = View.GONE + } + companion object { private const val TAG = "BaseActivity" diff --git a/main/src/main/java/com/example/main/user/adapter/UserFeedAdapter.kt b/main/src/main/java/com/example/main/user/adapter/UserFeedAdapter.kt index a08e8f4..d75a619 100644 --- a/main/src/main/java/com/example/main/user/adapter/UserFeedAdapter.kt +++ b/main/src/main/java/com/example/main/user/adapter/UserFeedAdapter.kt @@ -1,8 +1,12 @@ package com.example.main.user.adapter +import android.support.v7.widget.CardView import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater import android.view.ViewGroup +import com.example.core.extension.dp2px import com.example.core.model.UserFeed +import com.example.main.R import com.example.main.common.adapter.SimpleListFeedAdapter import com.example.main.user.ui.UserHomePageActivity @@ -13,21 +17,60 @@ import com.example.main.user.ui.UserHomePageActivity */ class UserFeedAdapter(override var activity: UserHomePageActivity, feedList: MutableList, maxImageWidth: Int, layoutManager: RecyclerView.LayoutManager) : SimpleListFeedAdapter(activity, feedList, maxImageWidth, layoutManager) { + + + override var isLoadFailed: Boolean = false + get() = activity.isLoadFailed + + override var isNoMoreData: Boolean = false + get() = activity.isNoMoreData + override fun onLoad() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + activity.onLoad() } override fun createFeedHolder(parent: ViewGroup): FeedViewHolder { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + val view = LayoutInflater.from(activity).inflate(R.layout.user_feed_item, parent, false) + val holder = FeedViewHolder(view) + initBaseFeedHolder(holder) + return holder } override fun createRefeedHolder(parent: ViewGroup): RefeedViewHolder { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + val view = LayoutInflater.from(activity).inflate(R.layout.user_refeed_item, parent, false) + val holder = RefeedViewHolder(view) + initBaseFeedHolder(holder) + return holder + } + + override fun bindFeedHolder(holder: FeedViewHolder, position: Int) { + setupFirstItemMarginTop(holder.cardView, position) + super.bindFeedHolder(holder, position) + } + + override fun bindRefeedHolder(holder: RefeedViewHolder, position: Int) { + setupFirstItemMarginTop(holder.cardView, position) + super.bindRefeedHolder(holder, position) + } + + private fun setupFirstItemMarginTop(cardView: CardView, position: Int) { + val params = if (position == 0) { + val layoutParams = cardView.layoutParams as RecyclerView.LayoutParams + layoutParams.topMargin = dp2px(35f) + layoutParams + } else { + val layoutParams = cardView.layoutParams as RecyclerView.LayoutParams + layoutParams.topMargin = dp2px(10f) + layoutParams + } + + cardView.layoutParams = params + + } + + companion object { + private const val TAG = "UserFeedAdapter" } - override var isLoadFailed: Boolean = false - get() = activity.isLoadFailed - override var isNoMoreData: Boolean = false - get() = activity.isNoMoreData } \ No newline at end of file diff --git a/main/src/main/java/com/example/main/user/ui/BrowserPhotoActivity.kt b/main/src/main/java/com/example/main/user/ui/BrowserPhotoActivity.kt new file mode 100644 index 0000000..0e03c06 --- /dev/null +++ b/main/src/main/java/com/example/main/user/ui/BrowserPhotoActivity.kt @@ -0,0 +1,35 @@ +package com.example.main.user.ui + +import android.app.Activity +import android.app.ActivityOptions +import android.content.Intent +import android.widget.ImageView +import com.example.core.util.AndroidVersion +import com.example.core.util.GlobalUtil +import com.example.main.R +import com.example.main.ui.BaseActivity + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午2:54 + * Describe:浏览用户大图 + */ +class BrowserPhotoActivity :BaseActivity(){ + + companion object{ + private const val TAG = "BrowserPhotoActivity" + + private const val URL = "url" + + fun actionStart(activity: Activity,url:String,imageView:ImageView){ + val intent=Intent(activity,BrowserPhotoActivity::class.java) + intent.putExtra(URL,url) + if(AndroidVersion.hasLollipop()){ + val options=ActivityOptions.makeSceneTransitionAnimation(activity,imageView, GlobalUtil.getString(R.string.transition_browse_photo)) + activity.startActivity(intent,options.toBundle()) + }else{ + activity.startActivity(intent) + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/user/ui/FollowshipActivity.kt b/main/src/main/java/com/example/main/user/ui/FollowshipActivity.kt new file mode 100644 index 0000000..bdfb150 --- /dev/null +++ b/main/src/main/java/com/example/main/user/ui/FollowshipActivity.kt @@ -0,0 +1,38 @@ +package com.example.main.user.ui + +import android.app.Activity +import android.content.Intent +import com.example.main.ui.BaseActivity + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午3:05 + * Describe:用户个人主页关注和粉丝列表的Activity + */ +class FollowshipActivity :BaseActivity(){ + + companion object{ + + const val NICKNAME = "NICKNAME" + + const val USER_ID = "USER_ID" + + const val IS_GET_FOLLOWINGS = "IS_GET_FOLLOWINGS" + + fun actionFollowings(activity: Activity, userId: Long, nickname: String){ + val intent= Intent(activity,FollowshipActivity::class.java) + intent.putExtra(USER_ID, userId) + intent.putExtra(NICKNAME, nickname) + intent.putExtra(IS_GET_FOLLOWINGS, true) + activity.startActivity(intent) + } + + fun actionFollowers(activity: Activity, userId: Long, nickname: String) { + val intent = Intent(activity, FollowshipActivity::class.java) + intent.putExtra(USER_ID, userId) + intent.putExtra(NICKNAME, nickname) + intent.putExtra(IS_GET_FOLLOWINGS, false) + activity.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/main/src/main/java/com/example/main/user/ui/UserHomePageActivity.kt b/main/src/main/java/com/example/main/user/ui/UserHomePageActivity.kt index a990102..77d8e32 100644 --- a/main/src/main/java/com/example/main/user/ui/UserHomePageActivity.kt +++ b/main/src/main/java/com/example/main/user/ui/UserHomePageActivity.kt @@ -1,16 +1,56 @@ package com.example.main.user.ui +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder import android.app.Activity +import android.app.AlertDialog +import android.content.Context import android.content.Intent +import android.content.res.ColorStateList import android.os.Bundle +import android.support.design.widget.AppBarLayout +import android.support.design.widget.CoordinatorLayout +import android.support.v4.content.ContextCompat +import android.support.v7.graphics.Palette import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView -import android.view.View +import android.support.v7.widget.SimpleItemAnimator +import android.telecom.Call +import android.text.TextUtils +import android.util.DisplayMetrics +import android.view.* import android.widget.LinearLayout +import com.example.bumptech.glide.Glide +import com.example.bumptech.glide.load.engine.DiskCacheStrategy +import com.example.bumptech.glide.load.resource.drawable.GlideDrawable +import com.example.bumptech.glide.request.RequestListener +import com.example.bumptech.glide.request.target.Target +import com.example.core.GifFun +import com.example.core.extension.* import com.example.core.model.UserFeed +import com.example.core.util.AndroidVersion +import com.example.core.util.GlobalUtil +import com.example.main.R +import com.example.main.common.callback.InfiniteScrollListener import com.example.main.common.callback.LoadDataListener +import com.example.main.event.* +import com.example.main.report.ReportActivity import com.example.main.ui.BaseActivity import com.example.main.user.adapter.UserFeedAdapter +import com.example.main.util.ColorUtils +import com.example.main.util.ResponseHandler +import com.example.main.util.UserUtil +import com.example.main.util.ViewUtils +import com.example.main.util.glide.CustomUrl +import com.quxianggif.network.model.* +import jp.wasbeef.glide.transformations.BlurTransformation +import jp.wasbeef.glide.transformations.CropCircleTransformation +import kotlinx.android.synthetic.main.activity_user_home_page.* +import kotlinx.android.synthetic.main.loading_footer.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.lang.Exception /** * Anthor: Zhuangmingzhu @@ -21,7 +61,7 @@ class UserHomePageActivity : BaseActivity(), LoadDataListener { private lateinit var layoutManager: LinearLayoutManager - private lateinit var adapter:UserFeedAdapter + private lateinit var adapter: UserFeedAdapter /** * RecyclerView的数据源,用于存储所有展示中的Feeds。 @@ -71,22 +111,652 @@ class UserHomePageActivity : BaseActivity(), LoadDataListener { private var isUserBgImageLoaded = false - var isNoMoreData=false + var isNoMoreData = false private set - var isLoadFailed=false + var isLoadFailed = false //判断是否正在加载Fedds - private var isLoading=false + private var isLoading = false + + //通过获取屏幕宽度计算出每张图片的宽度 + private val maxImageWidth: Int + get() { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + val metrics = DisplayMetrics() + windowManager.defaultDisplay?.getMetrics(metrics) + val columnWidth = metrics.widthPixels + return columnWidth - dp2px((24 + 20).toFloat()) + } + + //监听AppBarLayout的滑动,根据滑动的状态进行相应的界面效果切换 + private var titleOffsetChangeListener: AppBarLayout.OnOffsetChangedListener = object : AppBarLayout.OnOffsetChangedListener { + + var scrollRange = -1 + override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { + if(scrollRange==-1){ + scrollRange=appBarLayout.totalScrollRange + } + if(scrollRange+verticalOffset< dp2px(8f)){ + collapsingToolbar.title=mNickname + }else{ + collapsingToolbar.title=" " + } + if(!isUserBgImageLoaded){// 如果用户背景图还没加载出来,则不执行以下代码。 + return + } + if(collapsingToolbar.height+verticalOffset BrowserPhotoActivity.actionStart(it1, mAvatar, userAvatar) } + } + } + userFeedsText.setOnClickListener { appBar.setExpanded(false, true) } + userFollowingsText.setOnClickListener { FollowshipActivity.actionFollowings(this@UserHomePageActivity, mUserId, mNickname) } + userFollowersText.setOnClickListener { FollowshipActivity.actionFollowers(this@UserHomePageActivity, mUserId, mNickname) } + fab.setOnClickListener { + if (isCurrentUserHomePage) { + onEditFabClicked() + } else { + onFollowsFabClicked() + } + } + appBar.addOnOffsetChangedListener(titleOffsetChangeListener) + appBar.viewTreeObserver.addOnGlobalLayoutListener(object :ViewTreeObserver.OnGlobalLayoutListener{ + override fun onGlobalLayout() { + appBar.viewTreeObserver.removeOnGlobalLayoutListener(this) + // 数据没加载出来之前,需要先禁用AppBarLayout的滑动功能。 + setAppBarLayoutCanDrag(false) + } + + }) + + loadUserFeeds() + + } + + private fun onEditFabClicked() { + ModifyUserInfoActivity.actionStart(this) } + private val isCurrentUserHomePage: Boolean + get() = mUserId == GifFun.getUserId() + override fun onLoad() { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + isNoMoreData = false + isLoadFailed = false + loadUserFeeds() + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + if (menu != null && !isCurrentUserHomePage) { + val followUserItem = menu.findItem(R.id.follow_user) + val unfollowUserItem = menu.findItem(R.id.unfollow_user) + if (isFabFollowed) { + followUserItem.isEnabled = false + followUserItem.isVisible = false + unfollowUserItem.isEnabled = true + unfollowUserItem.isVisible = true + } else { + unfollowUserItem.isEnabled = false + unfollowUserItem.isVisible = false + followUserItem.isEnabled = true + followUserItem.isVisible = true + } + return true + } + return super.onPrepareOptionsMenu(menu) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + if (!isCurrentUserHomePage) { + menuInflater.inflate(R.menu.menu_user_home_page, menu) + return true + } + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.report_user -> { + ReportActivity.actionReportUser(this, mUserId) + return true + } + R.id.follow_user -> { + onFollowsFabClicked() + return true + } + R.id.unfollow_user -> { + onFollowsFabClicked() + return true + } + } + return super.onOptionsItemSelected(item) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + override fun onMessageEvent(messageEvent: MessageEvent) { + if (messageEvent is LikeFeedEvent) { + if (messageEvent.from == LikeFeedEvent.FROM_USER_HOME) { + return + } + val feedId = messageEvent.feedId + searchModelIndex(feedList, feedId) { index -> + val feed = feedList[index] + feed.isLikedAlready = messageEvent.type == LikeFeedEvent.LIKE_FEED + feed.likesCount = messageEvent.likesCount + adapter.notifyItemChanged(index) + } + } else if (messageEvent is DeleteFeedEvent) { + updateFeedCountAfterDelete() + if (feedList.isEmpty()) { + loadFinished() + } + } else if (messageEvent is ModifyUserInfoEvent) { + if (messageEvent.modifyNickname) { + mNickname = UserUtil.nickname + } + if (messageEvent.modifyAvatar) { + mAvatar = UserUtil.avatar + } + if (messageEvent.modifyBgImage) { + mBgImage = UserUtil.bgImage + } + if (messageEvent.modifyDescription) { + mDescription = UserUtil.description + } + if (messageEvent.modifyAvatar || messageEvent.modifyBgImage || messageEvent.modifyDescription || messageEvent.modifyNickname) { + setupUserInfo() + } + if (messageEvent.modifyNickname || messageEvent.modifyAvatar) { + feedList.clear() + loadUserFeeds() + } + } else if (messageEvent is LoadOriginAvatarEvent) { + // 用户浏览了头像大图,此时可以将个人主页的头像更新为清晰版。 + Glide.with(this) + .load(CustomUrl(mAvatar)) + .skipMemoryCache(true) + .bitmapTransform(CropCircleTransformation(this)) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .placeholder(R.drawable.loading_bg_circle) + .error(R.drawable.avatar_default) + .into(userAvatar) + } else { + super.onMessageEvent(messageEvent) + } + } + + /** + * 在删除Feed之后,更新用户的分享数量。 + */ + private fun updateFeedCountAfterDelete() { + try { + val content = userFeedsText.text.toString() + val numbers = content.getNumbersFromString() + var feedCount = Integer.parseInt(numbers) + userFeedsText.text = String.format(GlobalUtil.getString(R.string.user_feeds_count), --feedCount) + } catch (e: Exception) { + logWarn(TAG, e.message, e) + } + + } + + private fun loadUserFeeds() { + isLoading = true + var lastFeed: Long = 0 + val isLoadMore: Boolean + if (!feedList.isEmpty()) { + lastFeed = feedList[feedList.size - 1].feedId + isLoadMore = true + } else { + isLoadMore = false + } + + FetchUserFeeds.getResponse(mUserId, lastFeed, object : Callback { + override fun onResponse(response: Response) { + if (activity == null) { + return + } + handleFetchedFeeds(response, isLoadMore) + isLoading = false + } + + override fun onFailure(e: Exception) { + logWarn(TAG, e.message, e) + loadFailed(null) + if (!isLoadMore) { + ResponseHandler.handleFailure(e) + } + isLoading = false + } + }) + } + + //处理后去用户Feeds请求返回的结果 + private fun handleFetchedFeeds(response: Response, isLoadMore: Boolean) { + isNoMoreData = false + if (!ResponseHandler.handleResponse(response)) { + val fetchUserFeeds = response as FetchUserFeeds + val status = fetchUserFeeds.status + when (status) { + 0 -> { + val feeds = fetchUserFeeds.feeds + if (!isLoadMore) { + showUserInformation(fetchUserFeeds) + feedList.clear() + + } else { + recyclerView.stopScroll() + } + feedList.addAll(feeds) + adapter.notifyDataSetChanged() + loadFinished() + } + 1004 -> { + if (!isLoadMore) { + showUserInformation(fetchUserFeeds) + } + isNoMoreData = true + adapter.notifyItemChanged(adapter.dataItemCount) + loadFinished() + } + else -> { + logWarn(TAG, "Load user feeds failed. " + GlobalUtil.getResponseClue(status, fetchUserFeeds.msg)) + loadFailed(GlobalUtil.getString(R.string.fetch_data_failed) + ": " + response.status) + } + } + } else { + loadFailed(GlobalUtil.getString(R.string.unknown_error) + ": " + response.status) + } + } + + /** + * 将获取到的用户信息显示到界面上。 + */ + private fun showUserInformation(fetchUserFeeds: FetchUserFeeds) { + userFeedsText.text = String.format(GlobalUtil.getString(R.string.user_feeds_count), + GlobalUtil.getConvertedNumber(fetchUserFeeds.feedsCount)) + userFollowingsText.text = String.format(GlobalUtil.getString(R.string.user_followings_count), + GlobalUtil.getConvertedNumber(fetchUserFeeds.followingsCount)) + userFollowersText.text = String.format(GlobalUtil.getString(R.string.user_followers_count), + GlobalUtil.getConvertedNumber(fetchUserFeeds.followersCount)) + userCountsLayout.visibility = View.VISIBLE + isFollowed = fetchUserFeeds.isFollowing + mNickname = fetchUserFeeds.nickname + mDescription = fetchUserFeeds.description + mAvatar = fetchUserFeeds.avatar + mBgImage = fetchUserFeeds.bgImage + setupUserInfo() + popFab() + } + + /** + * 加载完成,将数据显示出来,将加载等待控件隐藏。 + */ + override fun loadFinished() { + super.loadFinished() + isLoadFailed = false + if (feedList.isEmpty()) { + progressBarLayout.visibility = View.GONE + recyclerView.visibility = View.GONE + if (isCurrentUserHomePage) { + showNoContentView(GlobalUtil.getString(R.string.you_posts_nothing)) + } else { + showNoContentView(GlobalUtil.getString(R.string.he_posts_nothing)) + } + setAppBarLayoutCanDrag(false) + } else { + progressBarLayout.visibility = View.GONE + recyclerView.visibility = View.VISIBLE + setAppBarLayoutCanDrag(true) + } + } + + //加载feeds失败,将加载等待控件隐藏 + override fun loadFailed(msg: String?) { + super.loadFailed(msg) + isLoadFailed = true + progressBarLayout.visibility = View.GONE + if (feedList.isEmpty()) { + setAppBarLayoutCanDrag(false) + if (msg == null) { + val refresh = Runnable { + startLoading() + progressBarLayout.visibility = View.VISIBLE + loadUserFeeds() + } + showBadNetworkView(View.OnClickListener { GifFun.getHandler().postDelayed(refresh, 400) }) + } else { + showLoadErrorView(msg) + } + } else { + adapter.notifyItemChanged(adapter.itemCount - 1) + } + } + + //设置AppBarLayout是否可拖动 + private fun setAppBarLayoutCanDrag(canDrag: Boolean) { + val params = appBar.layoutParams as CoordinatorLayout.LayoutParams + val behavior = params.behavior as AppBarLayout.Behavior? + behavior?.setDragCallback(object : AppBarLayout.Behavior.DragCallback() { + override fun canDrag(p0: AppBarLayout): Boolean { + return canDrag + } + }) + } + + //加载并显示用户个人主页上的基本信息 + private fun setupUserInfo() { + userNickname.text = mNickname + if (!TextUtils.isEmpty(mDescription)) { + userDescription.visibility = View.VISIBLE + userDescription.text = String.format(GlobalUtil.getString(R.string.description_content), mDescription) + } else { + userDescription.visibility = View.INVISIBLE + } + isUserBgImageLoaded = false + Glide.with(this) + .load(CustomUrl(mAvatar)) + .bitmapTransform(CropCircleTransformation(this)) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .placeholder(R.drawable.loading_bg_circle) + .error(R.drawable.avatar_default) + .into(userAvatar) + if (TextUtils.isEmpty(mBgImage)) { + if (!TextUtils.isEmpty(mAvatar)) { + Glide.with(this) + .load(CustomUrl(mAvatar)) + .bitmapTransform(BlurTransformation(this, 15)) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .listener(userBgLoadListener) + .into(userBgImage) + } + } else { + Glide.with(this) + .load(mBgImage) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .listener(userBgLoadListener) + .into(userBgImage) + } + } + + private var userBgLoadListener: RequestListener = object : RequestListener { + override fun onException(e: Exception?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + return false + } + + override fun onResourceReady(resource: GlideDrawable?, model: Any?, target: Target?, isFromMemoryCache: Boolean, isFirstResource: Boolean): Boolean { + if (resource == null) { + return false + } + val bitmap = resource.toBitmap() + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + if (bitmapWidth <= 0 || bitmapHeight <= 0) { + return false + } + Palette.from(bitmap) + .maximumColorCount(3) + .clearFilters() + .setRegion(0, 0, bitmapWidth - 1, (bitmapHeight * 0.1).toInt()) + .generate { palette -> + isUserBgImageDark = ColorUtils.isBitmapDark(palette, bitmap) + if (isUserBgImageDark) { + isToolbarAndStatusbarIconDark = false + setToolbarAndStatusbarIntoLight() + } else { + isToolbarAndStatusbarIconDark = true + setToolbarAndStatusbarIconIntoDark() + } + isUserBgImageLoaded = true + } + val left = (bitmapWidth * 0.2).toInt() + val right = bitmapWidth - left + val top = bitmapHeight / 2 + val bottom = bitmapHeight - 1 + Palette.from(bitmap) + .maximumColorCount(3) + .clearFilters() + .setRegion(left, top, right, bottom) + .generate { palette -> + val isDark = ColorUtils.isBitmapDark(palette, bitmap) + val color: Int + color = if (isDark) { + ContextCompat.getColor(this@UserHomePageActivity, R.color.white_text) + } else { + ContextCompat.getColor(this@UserHomePageActivity, R.color.black_text) + } + userNickname.setTextColor(color) + userFeedsText.setTextColor(color) + userDescription.setTextColor(color) + userFollowingsText.setTextColor(color) + userFollowersText.setTextColor(color) + } + return false + } + } + + //设置toolbar和状态栏上的图标颜色为浅色 + private fun setToolbarAndStatusbarIntoLight() { + ViewUtils.clearLightStatusBar(window, userBgImage) + toolbar?.let { ViewUtils.setToolbarIconColor(this, it, false) } + } + + /** + * 设置Toolbar和状态栏上的图标为深色。 + */ + private fun setToolbarAndStatusbarIconIntoDark() { + ViewUtils.setLightStatusBar(window, userBgImage) + toolbar?.let { ViewUtils.setToolbarIconColor(this, it, true) } + } + + private fun onFollowsFabClicked() { + if (!isFollowInProgress) { + if (isFollowed) { + unfollowUser() + } else { + followUser() + } + } + } + + //取关当前个人主页的用户 + private fun unfollowUser() { + val builder = AlertDialog.Builder(this, R.style.GifFunAlertDialogStyle) + builder.setMessage(GlobalUtil.getString(R.string.unfollow_confirm)) + builder.setPositiveButton(GlobalUtil.getString(R.string.ok)) { _, _ -> + setFabUnFollowed() + setFollowersCountChange(-1) + isFollowInProgress = true + UnfollowUser.getResponse(mUserId, object : Callback { + override fun onResponse(response: Response) { + isFollowInProgress = false + if (activity == null) { + return + } + if (response.status == 0) { + isFollowed = false + } else { + setFabFollowed() + setFollowersCountChange(1) + showToast("取关失败") + } + } + + override fun onFailure(e: Exception) { + isFollowInProgress = false + setFabFollowed() + setFollowersCountChange(1) + showToast("取关失败") + } + + }) + } + builder.setNegativeButton("取消", null) + builder.create().show() + } + + private fun followUser() { + setFabFollowed() + setFollowersCountChange(1) + isFollowInProgress = true + FollowUser.getResponse(mUserId, object : Callback { + override fun onResponse(response: Response) { + isFollowInProgress = false + if (activity == null) { + return + } + val status = response.status + if (status == 0) { + isFollowed = true + } else { + if (status == 10208) { + showToast(GlobalUtil.getString(R.string.follow_too_many)) + } else { + showToast(GlobalUtil.getString(R.string.follow_failed)) + } + setFabFollowed() + setFollowersCountChange(-1) + } + } + + override fun onFailure(e: Exception) { + isFollowInProgress = false + setFabUnFollowed() + setFollowersCountChange(-1) + showToast(GlobalUtil.getString(R.string.follow_failed)) + } + + }) + } + + /** + * 将当前界面的Fab按钮设置为未关注。 + */ + private fun setFabUnFollowed() { + fab.setImageResource(R.drawable.ic_follow) + fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorAccent)) + isFabFollowed = false + val event = FollowUserEvent() + event.userId = mUserId + event.type = FollowUserEvent.UNFOLLOW_USER + EventBus.getDefault().post(event) } + /** + * 将当前界面的Fab按钮设置为已关注。 + */ + private fun setFabFollowed() { + fab.setImageResource(R.drawable.ic_followed) + fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorPrimary)) + isFabFollowed = true + val event = FollowUserEvent() + event.userId = mUserId + event.type = FollowUserEvent.FOLLOW_USER + EventBus.getDefault().post(event) + } + + /** + * 当关注状态发生改变的时候,刷新当前界面用户的粉丝数量 + */ + private fun setFollowersCountChange(changeCount: Int) { + try { + val followerInfo = userFeedsText.text.toString() + val followerCount = Integer.parseInt(followerInfo.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1]) + userFollowersText.text = String.format(GlobalUtil.getString(R.string.user_followers_count), + followerCount + changeCount) + } catch (e: Exception) { + logError(TAG, e.message, e) + } + } + + //使用pop动画的方式将fab按钮显示出来 + private fun popFab() { + if (GifFun.getUserId() == mUserId) { + fab.setImageResource(R.drawable.ic_edit) + fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.colorAccent)) + } else { + if (isFollowed) { + setFabFollowed() + } else { + setFabUnFollowed() + } + } + fab.show() + fab.alpha = 0f + fab.scaleX = 0f + fab.scaleY = 0f + val animator = ObjectAnimator.ofPropertyValuesHolder( + fab, + PropertyValuesHolder.ofFloat(View.ALPHA, 1f), + PropertyValuesHolder.ofFloat(View.SCALE_X, 1f), + PropertyValuesHolder.ofFloat(View.SCALE_Y, 1f)) + animator.startDelay = 200 + animator.start() + + } + + companion object { private const val TAG = "UserHomePageActivity" diff --git a/main/src/main/res/drawable/user_home_page_bg_default.xml b/main/src/main/res/drawable/user_home_page_bg_default.xml new file mode 100644 index 0000000..7d9a8c2 --- /dev/null +++ b/main/src/main/res/drawable/user_home_page_bg_default.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/main/src/main/res/layout-v21/hot_feed_item.xml b/main/src/main/res/layout-v21/hot_feed_item.xml index d9e7202..a9fc7cf 100755 --- a/main/src/main/res/layout-v21/hot_feed_item.xml +++ b/main/src/main/res/layout-v21/hot_feed_item.xml @@ -84,7 +84,7 @@ android:layout_marginRight="10dp" android:layout_marginEnd="10dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/layout/hot_feed_item.xml b/main/src/main/res/layout/hot_feed_item.xml index d9e7202..a9fc7cf 100755 --- a/main/src/main/res/layout/hot_feed_item.xml +++ b/main/src/main/res/layout/hot_feed_item.xml @@ -84,7 +84,7 @@ android:layout_marginRight="10dp" android:layout_marginEnd="10dp"> - - - - + + + + + \ No newline at end of file diff --git a/main/src/main/res/values-v21/styles.xml b/main/src/main/res/values-v21/styles.xml index 9378199..32860da 100644 --- a/main/src/main/res/values-v21/styles.xml +++ b/main/src/main/res/values-v21/styles.xml @@ -17,4 +17,8 @@ @android:color/transparent + + \ No newline at end of file diff --git a/main/src/main/res/values-v21/transition_svg.xml b/main/src/main/res/values-v21/transition_svg.xml index 9c70813..7df2696 100644 --- a/main/src/main/res/values-v21/transition_svg.xml +++ b/main/src/main/res/values-v21/transition_svg.xml @@ -2,13 +2,41 @@ + transition_logo_splash + transition_login_dialog + transition_feed_detail + transition_feed_detail_bg + transition_feed_detail_image_bg + transition_user_home_page_avatar + transition_browse_photo + transition_search_back + M16.05,5 C14.484,5 12.981,5.70626703 12,6.81798365 C11.019,5.70626703 9.516,5 7.95,5 C5.1735,5 3,7.10572207 3,9.79564033 C3,13.0871935 6.06,15.7771117 10.695,19.853406 L12,21 L13.305,19.853406 C17.94,15.7771117 21,13.0871935 21,9.79564033 C21,7.10572207 18.8265,5 16.05,5 L16.05,5 Z M12.0945,18.5629428 L12,18.6457766 L11.9055,18.5629428 C7.626,14.800545 4.8,12.3155313 4.8,9.79564033 C4.8,8.05613079 6.1545,6.74386921 7.95,6.74386921 C9.336,6.74386921 10.686,7.61144414 11.1585,8.80163488 L12.837,8.80163488 C13.314,7.61144414 14.664,6.74386921 16.05,6.74386921 C17.8455,6.74386921 19.2,8.05613079 19.2,9.79564033 C19.2,12.3155313 16.374,14.800545 12.0945,18.5629428 L12.0945,18.5629428 Z M12,21 L10.695,19.853406 C6.06,15.7771117 3,13.0871935 3,9.79564033 C3,7.10572207 5.1735,5 7.95,5 C9.516,5 11.019,5.70626703 12,6.81798365 C12.981,5.70626703 14.484,5 16.05,5 C18.8265,5 21,7.10572207 21,9.79564033 C21,13.0871935 17.94,15.7771117 13.305,19.853406 L12,21 L12,21 Z M13.3843083,13.3956843 C11.233862,15.5399983 7.7581039,15.5381046 5.61000013,13.3900003 C3.46000004,11.2399998 3.46000004,7.76000023 5.61000013,5.61000013 C7.76000023,3.46000004 11.2400007,3.46000004 13.3900003,5.61000013 C15.54,7.76000023 15.5400009,11.2400007 13.3900003,13.3900003 C13.388104,13.3918967 13.3862067,13.3937913 13.3843083,13.3956843 C15.1427975,15.1834093 19.6826174,19.798706 19.6826172,19.7987061 L13.3843085,13.3956846 L13.3843083,13.3956843 Z + M24.7000008,12.6999998 C24.7000008,12.6999998 31.8173374,19.9066081 31.8173371,19.9066082 C32.7867437,20.7006357 34.4599991,23 37.5,23 C40.5400009,23 43,20.54 43,17.5 C43,14.46 40.5400009,12 37.5,12 C34.4599991,12 33.2173088,12 31.8173371,12 C31.8173374,12 18.8477173,12 18.8477173,12 + 0 + 0.185 + 0.75 + 1 + + 600 + 450 - transition_logo_splash - transition_feed_detail - transition_feed_detail_bg - transition_feed_detail_image_bg + + + M25.39,13.39 A 5.5 5.5 0 1 1 17.61 5.61 A 5.5 5.5 0 1 1 25.39 13.39 + 1 + 0 + 250 + 300 + + + M16.7017297,12.6957157 L24.7043962,4.69304955 + M16.7107986,11.2764828 L24.7221527,19.2878361 + 0 + 1 + 350 + 250 \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FetchUserFeeds.kt b/network/src/main/java/com/quxianggif/network/model/FetchUserFeeds.kt new file mode 100644 index 0000000..4dadffc --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FetchUserFeeds.kt @@ -0,0 +1,56 @@ +package com.quxianggif.network.model + +import com.example.core.model.UserFeed +import com.google.gson.annotations.SerializedName +import com.quxianggif.network.request.FetchUserFeedsRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/18 下午6:26 + * Describe:获取用户所发Feeds请求的实体类封装 + */ +class FetchUserFeeds :Response(){ + + @SerializedName("user_id") + var userId: Long = 0 + + var nickname: String = "" + + var avatar: String = "" + + @SerializedName("bg_image") + var bgImage: String = "" + + @SerializedName("feeds_count") + var feedsCount: Int = 0 + + @SerializedName("followings_count") + var followingsCount: Int = 0 + + @SerializedName("followers_count") + var followersCount: Int = 0 + + @SerializedName("is_following") + var isFollowing: Boolean = false + + var description: String = "" + + @SerializedName("data") + var feeds: MutableList = ArrayList() + + companion object{ + + fun getResponse(userId:Long,callback: Callback){ + FetchUserFeedsRequest() + .userId(userId) + .listen(callback) + } + + fun getResponse(userId: Long,lastFeed: Long,callback: Callback){ + FetchUserFeedsRequest() + .userId(userId) + .lastFeed(lastFeed) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/FollowUser.kt b/network/src/main/java/com/quxianggif/network/model/FollowUser.kt new file mode 100644 index 0000000..6e09251 --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/FollowUser.kt @@ -0,0 +1,26 @@ +package com.quxianggif.network.model + +import com.quxianggif.network.request.FollowUserRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午2:16 + * Describe:关注用户请求的实体类封装 + */ +class FollowUser :Response(){ + + companion object{ + + fun getResponse(userId:Long,callback: Callback){ + FollowUserRequest() + .followingIds(userId) + .listen(callback) + } + + fun getResponse(userIds:LongArray,callback: Callback){ + FollowUserRequest() + .followingIds(*userIds) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/model/UnfollowUser.kt b/network/src/main/java/com/quxianggif/network/model/UnfollowUser.kt new file mode 100644 index 0000000..1168b2b --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/model/UnfollowUser.kt @@ -0,0 +1,20 @@ +package com.quxianggif.network.model + +import com.quxianggif.network.request.UnfollowUserRequest + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午1:49 + * Describe:取消关注用户请求的额实体类 + */ +class UnfollowUser :Response(){ + + companion object{ + + fun getResponse(followingId:Long,callback: Callback){ + UnfollowUserRequest() + .followingId(followingId) + .listen(callback) + } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FetchUserFeedsRequest.kt b/network/src/main/java/com/quxianggif/network/request/FetchUserFeedsRequest.kt new file mode 100644 index 0000000..372688c --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FetchUserFeedsRequest.kt @@ -0,0 +1,68 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FetchUserFeeds +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/18 下午6:29 + * Describe:获取指定用户所发Feeds的请求,对应服务器接口:/feeds/user + */ +class FetchUserFeedsRequest : Request() { + + private var lastFeed: Long = 0 + + private var userId: Long = 0 + + fun userId(userId: Long): FetchUserFeedsRequest { + this.userId = userId + return this + } + + fun lastFeed(lastFeed: Long): FetchUserFeedsRequest { + this.lastFeed = lastFeed + return this + } + + + override fun method(): Int { + return GET + } + + override fun url(): String { + return URL + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FetchUserFeeds::class.java) + } + + override fun params(): Map? { + + val params=HashMap() + if(buildAuthParams(params)){ + if(lastFeed>0){ + params[NetworkConst.LAST_FEED]=lastFeed.toString() + } + if (userId > 0) { + params[NetworkConst.USER_ID] = userId.toString() + } + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.TOKEN,NetworkConst.USER_ID) + return super.headers(builder) + } + + companion object { + + private val URL = GifFun.BASE_URL + "/feeds/user" + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/FollowUserRequest.kt b/network/src/main/java/com/quxianggif/network/request/FollowUserRequest.kt new file mode 100644 index 0000000..fe8cfdc --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/FollowUserRequest.kt @@ -0,0 +1,64 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.FollowUser +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午2:18 + * Describe:关注用户请求。对应服务器接口:/user/follow + */ +class FollowUserRequest :Request(){ + + private var followingIds: LongArray? = null + + fun followingIds(vararg followingIds: Long): FollowUserRequest { + this.followingIds = followingIds + return this + } + + override fun url(): String { + return URL + } + + override fun method(): Int { + return POST + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(FollowUser::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + val followingsBuilder=StringBuilder() + var needComma=false + if(this.followingIds!=null&&this.followingIds!!.size>0){ + for(followingId in this.followingIds!!){ + if(needComma){ + followingsBuilder.append(",") + } + followingsBuilder.append(followingId) + needComma=true + } + } + params[NetworkConst.FOLLOWING_IDS]=followingsBuilder.toString() + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.FOLLOWING_IDS,NetworkConst.TOKEN) + return super.headers(builder) + } + + companion object { + private val URL = GifFun.BASE_URL + "/user/follow" + } +} \ No newline at end of file diff --git a/network/src/main/java/com/quxianggif/network/request/UnfollowUserRequest.kt b/network/src/main/java/com/quxianggif/network/request/UnfollowUserRequest.kt new file mode 100644 index 0000000..7d7150a --- /dev/null +++ b/network/src/main/java/com/quxianggif/network/request/UnfollowUserRequest.kt @@ -0,0 +1,54 @@ +package com.quxianggif.network.request + +import com.example.core.GifFun +import com.quxianggif.network.model.Callback +import com.quxianggif.network.model.UnfollowUser +import com.quxianggif.network.util.NetworkConst +import okhttp3.Headers + +/** + * Anthor: Zhuangmingzhu + * Date: 2019/7/19 下午1:52 + * Describe:取关用户请求,对应服务器接口:/user/unfollow + */ +class UnfollowUserRequest : Request() { + + private var followingId: Long = 0 + + fun followingId(followingId:Long):UnfollowUserRequest{ + this.followingId = followingId + return this + } + + override fun url(): String { + return URL + } + + override fun method(): Int { + return POST + } + + override fun listen(callback: Callback?) { + setListener(callback) + inFlight(UnfollowUser::class.java) + } + + override fun params(): Map? { + val params=HashMap() + if(buildAuthParams(params)){ + params[NetworkConst.FOLLOWING_ID]=followingId.toString() + return params + } + return super.params() + } + + override fun headers(builder: Headers.Builder): Headers.Builder { + buildAuthHeaders(builder,NetworkConst.FOLLOWING_ID,NetworkConst.TOKEN) + return super.headers(builder) + } + + companion object{ + private val URL = GifFun.BASE_URL + "/user/unfollow" + } + +} \ No newline at end of file