diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56..b86273d 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 21173a1..90b9ca7 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,7 @@
+
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7c2004b..5681bf6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -49,6 +49,8 @@ android {
dependencies {
implementation(project(":baselib"))
+ implementation(project(":medialib"))
+
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 905f14f..54c10b6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,25 +21,27 @@
android:networkSecurityConfig="@xml/network_config"
android:roundIcon="@drawable/app_logo"
android:supportsRtl="true"
- android:theme="@style/Theme.Aimz_k"
+ android:theme="@style/Anim_fade"
android:usesCleartextTraffic="true"
tools:targetApi="31">
-
-
+
+ android:theme="@style/Anim_fade">
+
+
+
,
- private val viewModel: MsgViewModel
+ datas: List, private val viewModel: MsgViewModel
) : BaseBindingAdapter(datas) {
override fun getItemBinding(parent: ViewGroup, viewType: Int): ItemCategoryBinding {
@@ -23,6 +24,21 @@ class CategoryAdapter(
holder.binding.root.setOnClickListener {
viewModel.onItemClickListener?.onItemClick(list[position])
}
+ val xPopup = XPopup.Builder(holder.binding.root.context).watchView(holder.binding.root)
+ holder.binding.root.setOnLongClickListener {
+ xPopup.asAttachList(
+ arrayOf("删除该聊天"),
+ intArrayOf(com.tenlionsoft.baselib.R.drawable.ic_del),
+ { p, _ ->
+ viewModel.onItemLongClickListener?.onItemLongClick(
+ p, list[position]
+ )
+ },
+ 0,
+ 0
+ ).show()
+ true
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgCategoryDao.kt b/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgCategoryDao.kt
index 48e631a..d51e70e 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgCategoryDao.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgCategoryDao.kt
@@ -58,6 +58,10 @@ interface MsgCategoryDao {
@Query("DELETE FROM db_category WHERE `senderId`=(:from) AND receiverId=(:to) OR `senderId`=(:to) AND receiverId=(:from)")
suspend fun delChatHistory(from: String?, to: String?)
+ //删除聊天
+ @Query("DELETE FROM db_category WHERE senderId=:senderId")
+ suspend fun delChatBySenderId(senderId: String)
+
/**
* 添加消息多个
*
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgDao.kt b/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgDao.kt
index 79f3883..c578d2e 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgDao.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/model/MsgDao.kt
@@ -87,8 +87,8 @@ interface MsgDao {
/**
* 清空与某个人的聊天记录
*/
- @Query("DELETE FROM db_msg WHERE `senderId`=(:from) AND `receiverId`=(:to) OR `receiverId`=(:to) AND `senderId`=(:from)")
- fun delChatHistory(from: String?, to: String?)
+ @Query("DELETE FROM db_msg WHERE senderId=:from AND receiverId=:to OR senderId=:from AND senderId=:to")
+ suspend fun delChatHistory(from: String, to: String)
/**
* 添加消息多个
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt b/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt
index fb559c3..cfe4b6a 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/page/activity/ChatActivity.kt
@@ -14,12 +14,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.atwa.filepicker.core.FilePicker
+import com.google.gson.Gson
import com.tenlionsoft.aimz_k.R
import com.tenlionsoft.aimz_k.databinding.ActivityChatBinding
+import com.tenlionsoft.aimz_k.model.BodyContent
+import com.tenlionsoft.aimz_k.model.FileDataBean
import com.tenlionsoft.aimz_k.model.MsgBean
import com.tenlionsoft.aimz_k.model.PickerType
import com.tenlionsoft.aimz_k.viewmodel.ChatPageViewModel
import com.tenlionsoft.baselib.base.BaseActivity
+import com.tenlionsoft.baselib.contacts.NetConfig
import com.tenlionsoft.baselib.contacts.ProjectConfig
import com.tenlionsoft.baselib.utils.DensityUtils
import com.tenlionsoft.baselib.utils.SpUtils
@@ -27,6 +31,8 @@ import com.tenlionsoft.baselib.widget.AdapterItemClickListener
import com.tenlionsoft.baselib.widget.LoadingDialog
import com.tenlionsoft.baselib.widget.SoftKeyBoardListener
import com.tenlionsoft.baselib.widget.wheel.WheelView
+import com.tenlionsoft.medialib.base.SimplePhotoActivity
+import com.tenlionsoft.medialib.base.SimpleVideoActivity
/**
@@ -105,11 +111,8 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener {
chatPageViewModel!!.showLoadDialog.observe(this) {
if (it) {
//显示loading
- mLoading = LoadingDialog.Builder(this)
- .setCancelOutside(false)
- .setCancelable(false)
- .setMessage("上传中...")
- .create()
+ mLoading = LoadingDialog.Builder(this).setCancelOutside(false).setCancelable(false)
+ .setMessage("上传中...").create()
mLoading!!.show()
} else {
//隐藏loading
@@ -163,8 +166,7 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener {
mBinding.rlvChats.postDelayed({
layoutManager.scrollToPositionWithOffset(
- itemCount - 1,
- 0
+ itemCount - 1, 0
)
}, 50)
chatPageViewModel!!.scrollListToBottom.value = false
@@ -182,38 +184,35 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener {
bottomHeight = mBinding.llBottomLayout.height
}
}
- SoftKeyBoardListener().setChangeListener(this@ChatActivity,
- { h ->
- Log.e("ChatActivity", "软键盘: 显示")
- chatPageViewModel!!.showReplyLayout.value = false
- chatPageViewModel!!.showEmojiLayout.value = false
- chatPageViewModel!!.showChooseLayout.value = false
- mBinding.rlBottom.visibility = View.VISIBLE
- //重置layout高度
+ SoftKeyBoardListener().setChangeListener(this@ChatActivity, { h ->
+ Log.e("ChatActivity", "软键盘: 显示")
+ chatPageViewModel!!.showReplyLayout.value = false
+ chatPageViewModel!!.showEmojiLayout.value = false
+ chatPageViewModel!!.showChooseLayout.value = false
+ mBinding.rlBottom.visibility = View.VISIBLE
+ //重置layout高度
+ val layoutParams = mBinding.rlvChats.layoutParams
+ layoutParams.height = mContentHeight - h
+ mBinding.rlvChats.layoutParams = layoutParams
+ val layoutManager = mBinding.rlvChats.layoutManager as LinearLayoutManager
+ val itemCount = layoutManager.itemCount
+ //滚动到底部
+ mBinding.rlvChats.postDelayed({
+ layoutManager.scrollToPositionWithOffset(
+ itemCount - 1, 0
+ )
+ }, 100)
+ }, { _ ->
+ Log.e("ChatActivity", "软键盘: 隐藏")
+ val isShowOther =
+ chatPageViewModel!!.showReplyLayout.value!! || chatPageViewModel!!.showEmojiLayout.value!! || chatPageViewModel!!.showChooseLayout.value!!
+ if (!isShowOther) {
val layoutParams = mBinding.rlvChats.layoutParams
- layoutParams.height = mContentHeight - h
+ layoutParams.height = mContentHeight
mBinding.rlvChats.layoutParams = layoutParams
- val layoutManager = mBinding.rlvChats.layoutManager as LinearLayoutManager
- val itemCount = layoutManager.itemCount
- //滚动到底部
- mBinding.rlvChats.postDelayed({
- layoutManager.scrollToPositionWithOffset(
- itemCount - 1,
- 0
- )
- }, 100)
- },
- { _ ->
- Log.e("ChatActivity", "软键盘: 隐藏")
- val isShowOther =
- chatPageViewModel!!.showReplyLayout.value!! || chatPageViewModel!!.showEmojiLayout.value!! || chatPageViewModel!!.showChooseLayout.value!!
- if (!isShowOther) {
- val layoutParams = mBinding.rlvChats.layoutParams
- layoutParams.height = mContentHeight
- mBinding.rlvChats.layoutParams = layoutParams
- mBinding.rlBottom.visibility = View.GONE
- }
- })
+ mBinding.rlBottom.visibility = View.GONE
+ }
+ })
registerLocalReceiver()
}
@@ -242,8 +241,7 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener {
//滚动到底部
mBinding.rlvChats.postDelayed({
layoutManager.scrollToPositionWithOffset(
- itemCount - 1,
- 0
+ itemCount - 1, 0
)
}, 100)
}
@@ -274,6 +272,34 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener {
//条目点击
override fun onItemClick(data: MsgBean) {
+ when (data.messageType) {
+ ProjectConfig.MSG_TEXT -> {}
+ ProjectConfig.MSG_IMG -> {
+ val intent = Intent(this@ChatActivity, SimplePhotoActivity::class.java)
+ val gson = Gson()
+ val body = gson.fromJson(data.body, BodyContent::class.java)
+ val fileDataBean = gson.fromJson(body.content, FileDataBean::class.java)
+ val url = NetConfig.MAIN_URL + fileDataBean.fileUrl
+ val urls = arrayListOf(url)
+ intent.putStringArrayListExtra(SimplePhotoActivity.TAG_IMGURL, urls)
+ startActivity(intent)
+ }
+
+ ProjectConfig.MSG_FILE -> {
+ //TODO 下载文集,调用系统打开
+
+ }
+
+ ProjectConfig.MSG_VIDEO -> {
+ val intent = Intent(this@ChatActivity, SimpleVideoActivity::class.java)
+ val gson = Gson()
+ val body = gson.fromJson(data.body, BodyContent::class.java)
+ val fileDataBean = gson.fromJson(body.content, FileDataBean::class.java)
+ intent.putExtra("url", fileDataBean.fileUrl)
+ intent.putExtra("title", fileDataBean.fileName)
+ startActivity(intent)
+ }
+ }
}
}
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/page/fragments/MsgFragment.kt b/app/src/main/java/com/tenlionsoft/aimz_k/page/fragments/MsgFragment.kt
index 3156f64..802aac4 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/page/fragments/MsgFragment.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/page/fragments/MsgFragment.kt
@@ -1,5 +1,6 @@
package com.tenlionsoft.aimz_k.page.fragments
+import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -8,7 +9,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
import com.tenlionsoft.aimz_k.R
import com.tenlionsoft.aimz_k.databinding.FragmentMsgBinding
import com.tenlionsoft.aimz_k.model.MsgCategoryBean
@@ -18,24 +20,21 @@ import com.tenlionsoft.aimz_k.services.SocketService
import com.tenlionsoft.aimz_k.viewmodel.MsgViewModel
import com.tenlionsoft.baselib.contacts.ProjectConfig
import com.tenlionsoft.baselib.widget.AdapterItemClickListener
+import com.tenlionsoft.baselib.widget.AdapterItemLongClickListener
import com.tenlionsoft.baselib.widget.LoadingDialog
class MsgFragment : Fragment(), AdapterItemClickListener,
- MainActivity.OnSocketConnectListener {
+ MainActivity.OnSocketConnectListener, AdapterItemLongClickListener {
private lateinit var mMsgBinding: FragmentMsgBinding
private var mActivity: MainActivity? = null
- private var mLoading: LoadingDialog? = null;
+ private var mLoading: LoadingDialog? = null
companion object {
fun newInstance() = MsgFragment()
}
- private val viewModel: MsgViewModel by viewModels()
+ private lateinit var viewModel: MsgViewModel
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -46,15 +45,21 @@ class MsgFragment : Fragment(), AdapterItemClickListener,
R.layout.fragment_msg,
container,
false
- );
+ )
mMsgBinding.llSearchLayout.llSearchLayout.setOnClickListener {
startActivity(Intent(this@MsgFragment.context, ChatActivity::class.java))
}
+ viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return MsgViewModel() as T
+ }
+ })[MsgViewModel::class.java]
mMsgBinding.msgViewModel = viewModel
mMsgBinding.lifecycleOwner = this
viewModel.onItemClickListener = this//条目点击
- initView();
- return mMsgBinding.root;
+ viewModel.onItemLongClickListener = this//长按
+ initView()
+ return mMsgBinding.root
}
/**
@@ -79,6 +84,19 @@ class MsgFragment : Fragment(), AdapterItemClickListener,
mMsgBinding.llSearchLayout.pbSocketLoading.visibility = View.GONE
}
}
+ viewModel.showLoadDialog.observe(viewLifecycleOwner) {
+ if (it) {
+ //显示loading
+ mLoading = LoadingDialog.Builder(mActivity as Activity).setCancelOutside(false)
+ .setCancelable(false)
+ .setMessage("删除中...").create()
+ mLoading!!.show()
+ } else {
+ //隐藏loading
+ mLoading?.dismiss()
+ mLoading = null
+ }
+ }
//登陆socket
viewModel.doLoginWebsocket()
}
@@ -104,11 +122,11 @@ class MsgFragment : Fragment(), AdapterItemClickListener,
/**
* 条目点击
*/
- override fun onItemClick(msgCategoryBean: MsgCategoryBean) {
+ override fun onItemClick(data: MsgCategoryBean) {
startActivity(
Intent(mActivity, ChatActivity::class.java).putExtra(
"fromId",
- msgCategoryBean.senderId
+ data.senderId
)
)
}
@@ -133,4 +151,10 @@ class MsgFragment : Fragment(), AdapterItemClickListener,
}
}
+ //长按
+ override fun onItemLongClick(type: Int, d: MsgCategoryBean) {
+ //删除聊天
+ viewModel.doDelChat(d)
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/services/SocketService.kt b/app/src/main/java/com/tenlionsoft/aimz_k/services/SocketService.kt
index 414de22..bd80b8b 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/services/SocketService.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/services/SocketService.kt
@@ -8,6 +8,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
import android.util.Log
@@ -26,6 +27,7 @@ class SocketService : Service(), WsManager.MsgCallBack {
private var mWsManager: WsManager? = null
private val gson = Gson()
private lateinit var mLocalReceiver: LocalReceiver
+ private lateinit var mMediaPlayer: MediaPlayer
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -54,6 +56,7 @@ class SocketService : Service(), WsManager.MsgCallBack {
// startForeground(startId, notification)
this.startSocket();//开启Socket
+ mMediaPlayer = MediaPlayer.create(this, com.tenlionsoft.baselib.R.raw.chat)
registerLocalReceiver()
return START_STICKY
}
@@ -98,6 +101,7 @@ class SocketService : Service(), WsManager.MsgCallBack {
override fun onDestroy() {
stopSocket();
unregisterReceiver(mLocalReceiver)
+ mMediaPlayer.release()
super.onDestroy()
}
@@ -123,9 +127,12 @@ class SocketService : Service(), WsManager.MsgCallBack {
//socket接收到信息
override fun onCallBackMsg(str: String) {
- Log.e("SocketService", "接收信息: ${str}")
+ Log.e("SocketService", "接收信息:${str}")
val intent = Intent(ProjectConfig.A_S_MSG_RECEIVER)
intent.putExtra("msg", str)
sendBroadcast(intent)
+ if (!str.contains("STATUS")) {
+ mMediaPlayer.start()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/ChatPageViewModel.kt b/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/ChatPageViewModel.kt
index 5decb01..c300ef4 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/ChatPageViewModel.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/ChatPageViewModel.kt
@@ -43,7 +43,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
class ChatPageViewModel(
private val fromId: String,
private val toId: String,
- private val context: Context
+ private var context: Context
) : BaseViewModel() {
val txtMsg = MutableLiveData("")
val showSendBtn = MutableLiveData(false)//显示/隐藏发送按钮
@@ -284,6 +284,7 @@ class ChatPageViewModel(
_msgList.value = _msgList.value?.plus(msgBean)
adapter.setData(_msgList.value!!)
}
+ sendHandlerToLooper(b)
intent.putExtra("msgBean", b)
context.sendBroadcast(intent)
scrollListToBottom.value = true
diff --git a/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/MsgViewModel.kt b/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/MsgViewModel.kt
index 94158fe..e27e6d0 100644
--- a/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/MsgViewModel.kt
+++ b/app/src/main/java/com/tenlionsoft/aimz_k/viewmodel/MsgViewModel.kt
@@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.tenlionsoft.aimz_k.ConvertBeanUtils
@@ -16,18 +15,22 @@ import com.tenlionsoft.aimz_k.model.MsgCategoryBean
import com.tenlionsoft.aimz_k.model.MsgConvertBean
import com.tenlionsoft.aimz_k.model.Receiver
import com.tenlionsoft.aimz_k.model.Sender
+import com.tenlionsoft.baselib.base.BaseViewModel
import com.tenlionsoft.baselib.contacts.ProjectConfig
import com.tenlionsoft.baselib.utils.SpUtils
import com.tenlionsoft.baselib.utils.TimeUtils
+import com.tenlionsoft.baselib.utils.ToastUtils
import com.tenlionsoft.baselib.widget.AdapterItemClickListener
+import com.tenlionsoft.baselib.widget.AdapterItemLongClickListener
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-class MsgViewModel : ViewModel() {
+class MsgViewModel : BaseViewModel() {
private val _pwdList = MutableLiveData>()
var adapter: CategoryAdapter = CategoryAdapter(_pwdList.value ?: emptyList(), this)
var onItemClickListener: AdapterItemClickListener? = null
+ var onItemLongClickListener: AdapterItemLongClickListener? = null
val isLogining = MutableLiveData(false)
private val mGson = Gson()
private var mCurrentPage: Int = 1
@@ -157,4 +160,20 @@ class MsgViewModel : ViewModel() {
fun refresh() {
getList(true)
}
+
+ //删除聊天
+ fun doDelChat(d: MsgCategoryBean) {
+ viewModelScope.launch {
+ showLoadDialog.value = true
+ withContext(Dispatchers.IO) {
+ val cDao = DbManager.db.categoryDao()
+ val mDao = DbManager.db.msgDao()
+ cDao.delChatBySenderId(d.senderId!!)
+ mDao.delChatHistory(d.senderId!!, d.receiverId!!)
+ }
+ showLoadDialog.value = false
+ ToastUtils.success("删除成功")
+ refresh()
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 25b9d38..877613f 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -2,4 +2,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/baselib/build.gradle.kts b/baselib/build.gradle.kts
index 1968211..3a21888 100644
--- a/baselib/build.gradle.kts
+++ b/baselib/build.gradle.kts
@@ -44,7 +44,6 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
-
api(libs.rxjava)
api(libs.rxandroid)
implementation(libs.mmkv)
@@ -67,4 +66,5 @@ dependencies {
api(libs.xxpermissions)
api(libs.filepicker)
api(libs.room.ktx)
+ api(libs.popup)
}
\ No newline at end of file
diff --git a/baselib/src/main/java/com/tenlionsoft/baselib/widget/AdapterItemLongClickListener.kt b/baselib/src/main/java/com/tenlionsoft/baselib/widget/AdapterItemLongClickListener.kt
new file mode 100644
index 0000000..0342209
--- /dev/null
+++ b/baselib/src/main/java/com/tenlionsoft/baselib/widget/AdapterItemLongClickListener.kt
@@ -0,0 +1,5 @@
+package com.tenlionsoft.baselib.widget
+
+interface AdapterItemLongClickListener {
+ fun onItemLongClick(type: Int, d: D)
+}
\ No newline at end of file
diff --git a/baselib/src/main/res/anim/fade_in.xml b/baselib/src/main/res/anim/fade_in.xml
new file mode 100644
index 0000000..a3c90ff
--- /dev/null
+++ b/baselib/src/main/res/anim/fade_in.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/baselib/src/main/res/anim/fade_out.xml b/baselib/src/main/res/anim/fade_out.xml
new file mode 100644
index 0000000..7a83fb7
--- /dev/null
+++ b/baselib/src/main/res/anim/fade_out.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/baselib/src/main/res/anim/slide_in_right.xml b/baselib/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 0000000..7a4ddb0
--- /dev/null
+++ b/baselib/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/baselib/src/main/res/anim/slide_out_left.xml b/baselib/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 0000000..c646592
--- /dev/null
+++ b/baselib/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/baselib/src/main/res/drawable-xhdpi/ic_del.png b/baselib/src/main/res/drawable-xhdpi/ic_del.png
new file mode 100644
index 0000000..d882852
Binary files /dev/null and b/baselib/src/main/res/drawable-xhdpi/ic_del.png differ
diff --git a/baselib/src/main/res/drawable-xhdpi/ic_img_loading.png b/baselib/src/main/res/drawable-xhdpi/ic_img_loading.png
new file mode 100644
index 0000000..530b9d3
Binary files /dev/null and b/baselib/src/main/res/drawable-xhdpi/ic_img_loading.png differ
diff --git a/baselib/src/main/res/layout/dialog_loading.xml b/baselib/src/main/res/layout/dialog_loading.xml
index 912134c..317f449 100644
--- a/baselib/src/main/res/layout/dialog_loading.xml
+++ b/baselib/src/main/res/layout/dialog_loading.xml
@@ -1,32 +1,32 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/baselib/src/main/res/raw/chat.mp3 b/baselib/src/main/res/raw/chat.mp3
new file mode 100644
index 0000000..5cdb693
Binary files /dev/null and b/baselib/src/main/res/raw/chat.mp3 differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fcb60db..a76f851 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -37,8 +37,8 @@ emoji2Emojipicker = "1.0.0-alpha03"
xxpermissions = "20.0"
filepicker = "2.0.0"
room-ktx = "2.2.5"
-
-
+ijkplayer = "v10.0.0"
+popup = "2.10.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -87,9 +87,15 @@ emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.
xxpermissions = { group = "com.github.getActivity", name = "XXPermissions", version.ref = "xxpermissions" }
filepicker = { group = "com.github.atwa", name = "filepicker", version.ref = "filepicker" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-ktx" }
+ijkplayer = { group = "com.github.CarGuo.GSYVideoPlayer", name = "gsyvideoplayer-java", version.ref = "ijkplayer" }
+#ijk模式的so
+ijkplayer-arm64 = { group = "com.github.CarGuo.GSYVideoPlayer", name = "gsyvideoplayer-arm64", version.ref = "ijkplayer" }
+ijkplayer-armv7a = { group = "com.github.CarGuo.GSYVideoPlayer", name = "gsyvideoplayer-armv7a", version.ref = "ijkplayer" }
+# popup
+popup = { group = "com.github.li-xiaojun", name = "XPopup", version.ref = "popup" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.0.21" }
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kapt" }
android-library = { id = "com.android.library", version.ref = "agp" }
diff --git a/medialib/.gitignore b/medialib/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/medialib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/medialib/build.gradle.kts b/medialib/build.gradle.kts
new file mode 100644
index 0000000..a997f3c
--- /dev/null
+++ b/medialib/build.gradle.kts
@@ -0,0 +1,60 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "com.tenlionsoft.medialib"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ ndk {
+ abiFilters.addAll(arrayOf("armeabi", "armeabi-v7a", "x86", "x86_64", "arm64-v8a"))
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ buildFeatures {
+ viewBinding = true
+ dataBinding = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.1"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.core.ktx)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ api(libs.ijkplayer)
+ api(libs.ijkplayer.arm64)
+ api(libs.ijkplayer.armv7a)
+ api(project(":baselib"))
+
+}
\ No newline at end of file
diff --git a/medialib/consumer-rules.pro b/medialib/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/medialib/proguard-rules.pro b/medialib/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/medialib/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/medialib/src/androidTest/java/com/tenlionsoft/medialib/ExampleInstrumentedTest.java b/medialib/src/androidTest/java/com/tenlionsoft/medialib/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..b53f9fd
--- /dev/null
+++ b/medialib/src/androidTest/java/com/tenlionsoft/medialib/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.tenlionsoft.medialib;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.tenlionsoft.medialib.test", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/AndroidManifest.xml b/medialib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..257a975
--- /dev/null
+++ b/medialib/src/main/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/SimplePhotoActivity.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/SimplePhotoActivity.kt
new file mode 100644
index 0000000..f6e072a
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/SimplePhotoActivity.kt
@@ -0,0 +1,61 @@
+package com.tenlionsoft.medialib.base
+
+import androidx.databinding.DataBindingUtil
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import com.tenlionsoft.baselib.base.BaseActivity
+import com.tenlionsoft.baselib.utils.ToastUtils
+import com.tenlionsoft.medialib.R
+import com.tenlionsoft.medialib.base.adapter.PhotoAdapter
+import com.tenlionsoft.medialib.databinding.ActivitySimplePhotoBinding
+
+class SimplePhotoActivity : BaseActivity() {
+ private lateinit var mBind: ActivitySimplePhotoBinding
+ private var mImgUrls: java.util.ArrayList? = null
+ override fun bindView() {
+ mBind = DataBindingUtil.setContentView(
+ this@SimplePhotoActivity, R.layout.activity_simple_photo
+ )
+ mImgUrls = intent.getStringArrayListExtra(TAG_IMGURL)
+ val curItem = intent.getIntExtra("curItem", 0)
+ if (mImgUrls == null) {
+ ToastUtils.error("图片链接地址有误")
+ finish()
+ } else {
+ val imageAdapter: PhotoAdapter = PhotoAdapter(this, mImgUrls!!)
+ mBind.vpImages.setAdapter(imageAdapter)
+ imageAdapter.addImgClick(object : PhotoAdapter.OnImgClick {
+ override fun imgClick() {
+ finish()
+ }
+ })
+ mBind.tvCount.text =
+ String.format(
+ resources.getString(R.string.img_position), (curItem + 1), mImgUrls!!.size
+ )
+
+ mBind.vpImages.setCurrentItem(curItem)
+
+ mBind.vpImages.addOnPageChangeListener(object : OnPageChangeListener {
+ override fun onPageScrolled(
+ position: Int, positionOffset: Float, positionOffsetPixels: Int
+ ) {
+ }
+
+ override fun onPageSelected(position: Int) {
+ var pos = position
+ mBind.tvCount.text = String.format(
+ resources.getString(R.string.img_position), ++pos, mImgUrls!!.size
+ )
+
+ }
+
+ override fun onPageScrollStateChanged(state: Int) {
+ }
+ })
+ }
+ }
+
+ companion object {
+ const val TAG_IMGURL: String = "imgUrls"
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/SimpleVideoActivity.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/SimpleVideoActivity.kt
new file mode 100644
index 0000000..de0ef99
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/SimpleVideoActivity.kt
@@ -0,0 +1,96 @@
+package com.tenlionsoft.medialib.base
+
+import android.view.View
+import android.view.WindowManager
+import androidx.core.content.ContextCompat
+import androidx.databinding.DataBindingUtil
+import com.shuyu.gsyvideoplayer.GSYVideoManager
+import com.shuyu.gsyvideoplayer.utils.OrientationUtils
+import com.tenlionsoft.baselib.base.BaseActivity
+import com.tenlionsoft.baselib.contacts.NetConfig
+import com.tenlionsoft.baselib.utils.ToastUtils
+import com.tenlionsoft.medialib.databinding.ActivitySimpleVideoBinding
+
+
+class SimpleVideoActivity : BaseActivity() {
+ private lateinit var mBind: ActivitySimpleVideoBinding
+ private lateinit var orientationUtils: OrientationUtils
+ override fun bindView() {
+ mBind = DataBindingUtil.setContentView(
+ this@SimpleVideoActivity, com.tenlionsoft.medialib.R.layout.activity_simple_video
+ )
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ window.statusBarColor = ContextCompat.getColor(this, com.tenlionsoft.baselib.R.color.black)
+ val source = intent.getStringExtra("url")
+ val title = intent.getStringExtra("title") ?: "视频预览"
+ if (source.isNullOrEmpty()) {
+ ToastUtils.error("视频地址有误")
+ finish()
+ return
+ }
+ mBind.sVideo.setUp(NetConfig.MAIN_URL + source, true, title)
+
+
+ //增加封面
+// val imageView = ImageView(this)
+// imageView.scaleType = ImageView.ScaleType.CENTER_CROP
+// imageView.setImageResource(com.tenlionsoft.medialib.R.mipmap.xxx1)
+// videoPlayer.setThumbImageView(imageView)
+
+ //增加title
+ mBind.sVideo.titleTextView.visibility = View.VISIBLE
+
+ //设置返回键
+ mBind.sVideo.backButton.visibility = View.VISIBLE
+
+ //设置旋转
+ orientationUtils = OrientationUtils(this, mBind.sVideo)
+
+ //设置全屏按键功能,这是使用的是选择屏幕,而不是全屏
+ mBind.sVideo.getFullscreenButton().setOnClickListener(View.OnClickListener {
+ // ------- !!!如果不需要旋转屏幕,可以不调用!!!-------
+ // 不需要屏幕旋转,还需要设置 setNeedOrientationUtils(false)
+ orientationUtils.resolveByClick()
+ //finish();
+ })
+
+ //是否可以滑动调整
+ mBind.sVideo.setIsTouchWiget(true)
+
+ //设置返回按键功能
+ mBind.sVideo.getBackButton().setOnClickListener(View.OnClickListener { onBackPressed() })
+
+
+ /**不需要屏幕旋转 */
+ mBind.sVideo.setNeedOrientationUtils(false)
+
+ mBind.sVideo.startPlayLogic()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mBind.sVideo.onVideoResume()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mBind.sVideo.onVideoPause()
+ }
+
+ override fun onBackPressed() {
+ /** 不需要回归竖屏 */
+// if (orientationUtils.getScreenType() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+// videoPlayer.getFullscreenButton().performClick();
+// return;
+// }
+ //释放所有
+ mBind.sVideo.setVideoAllCallBack(null)
+ super.onBackPressed()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ GSYVideoManager.releaseAllVideos()
+ orientationUtils.releaseListener()
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/adapter/PhotoAdapter.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/adapter/PhotoAdapter.kt
new file mode 100644
index 0000000..e78db09
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/adapter/PhotoAdapter.kt
@@ -0,0 +1,57 @@
+package com.tenlionsoft.medialib.base.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.viewpager.widget.PagerAdapter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import com.tenlionsoft.medialib.R
+import com.tenlionsoft.medialib.base.widget.PhotoView
+
+class PhotoAdapter(private val ctx: Context, private val imgs: MutableList) :
+ PagerAdapter() {
+
+ private var mOnImgClick: OnImgClick? = null
+ override fun instantiateItem(container: ViewGroup, position: Int): Any {
+ val view: View = LayoutInflater.from(ctx)
+ .inflate(R.layout.item_photo, container, false)
+ val photoView = view.findViewById(R.id.pv_view)
+ val options: RequestOptions =
+ RequestOptions().error(com.tenlionsoft.baselib.R.drawable.ic_img_load_err)
+ .placeholder(com.tenlionsoft.baselib.R.drawable.ic_img_loading)
+ Glide.with(ctx)
+ .load(imgs.get(position))
+ .apply(options)
+ .into(photoView)
+ container.addView(view)
+ if (mOnImgClick != null) {
+ photoView.setOnClickListener { _ -> mOnImgClick?.imgClick() }
+ }
+ return view
+ }
+
+
+ fun addImgClick(onImgClick: OnImgClick?) {
+ this.mOnImgClick = onImgClick
+ }
+
+ interface OnImgClick {
+ fun imgClick()
+ }
+
+
+ override fun getCount(): Int {
+ return imgs.size
+ }
+
+
+ override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
+ container.removeView(`object` as View)
+ }
+
+ override fun isViewFromObject(view: View, `object`: Any): Boolean {
+ return view == `object`
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/Compat.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/Compat.kt
new file mode 100644
index 0000000..a6a53c4
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/Compat.kt
@@ -0,0 +1,17 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.view.View
+
+class Compat {
+ companion object {
+ const val SIXTY_FPS_INTERVAL: Int = 1000 / 60
+
+ fun postOnAnimation(view: View, runnable: Runnable) {
+ postOnAnimationJellyBean(view, runnable)
+ }
+
+ private fun postOnAnimationJellyBean(view: View, runnable: Runnable) {
+ view.postOnAnimation(runnable)
+ }
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/CustomGestureDetector.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/CustomGestureDetector.kt
new file mode 100644
index 0000000..dbea04f
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/CustomGestureDetector.kt
@@ -0,0 +1,193 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.ScaleGestureDetector.OnScaleGestureListener
+import android.view.VelocityTracker
+import android.view.ViewConfiguration
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.sqrt
+
+class CustomGestureDetector(private val context: Context, listener: OnGestureListener) {
+ val INVALID_POINTER_ID: Int = -1
+
+ private var mActivePointerId = INVALID_POINTER_ID
+ private var mActivePointerIndex = 0
+ private var mDetector: ScaleGestureDetector? = null
+
+ private var mVelocityTracker: VelocityTracker? = null
+ private var mIsDragging = false
+ private var mLastTouchX = 0f
+ private var mLastTouchY = 0f
+ private var mTouchSlop = 0f
+ private var mMinimumVelocity = 0f
+ private var mListener: OnGestureListener? = null
+
+ init {
+ val configuration = ViewConfiguration
+ .get(context)
+ mMinimumVelocity = configuration.scaledMinimumFlingVelocity.toFloat()
+ mTouchSlop = configuration.scaledTouchSlop.toFloat()
+
+ mListener = listener
+ val mScaleListener: OnScaleGestureListener = object : OnScaleGestureListener {
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ val scaleFactor = detector.scaleFactor
+
+ if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false
+
+ mListener?.onScale(
+ scaleFactor,
+ detector.focusX, detector.focusY
+ )
+ return true
+ }
+
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
+ return true
+ }
+
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
+ // NO-OP
+ }
+ }
+ mDetector = ScaleGestureDetector(context, mScaleListener)
+ }
+
+ private fun getActiveX(ev: MotionEvent): Float {
+ return try {
+ ev.getX(mActivePointerIndex)
+ } catch (e: Exception) {
+ ev.x
+ }
+ }
+
+ private fun getActiveY(ev: MotionEvent): Float {
+ return try {
+ ev.getY(mActivePointerIndex)
+ } catch (e: Exception) {
+ ev.y
+ }
+ }
+
+ fun isScaling(): Boolean {
+ return mDetector!!.isInProgress
+ }
+
+ fun isDragging(): Boolean {
+ return mIsDragging
+ }
+
+ fun onTouchEvent(ev: MotionEvent): Boolean {
+ try {
+ mDetector!!.onTouchEvent(ev)
+ return processTouchEvent(ev)
+ } catch (e: IllegalArgumentException) {
+ // Fix for support lib bug, happening when onDestroy is called
+ return true
+ }
+ }
+
+ private fun processTouchEvent(ev: MotionEvent): Boolean {
+ val action = ev.action
+ when (action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN -> {
+ mActivePointerId = ev.getPointerId(0)
+
+ mVelocityTracker = VelocityTracker.obtain()
+ mVelocityTracker?.addMovement(ev)
+
+ mLastTouchX = getActiveX(ev)
+ mLastTouchY = getActiveY(ev)
+ mIsDragging = false
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ val x = getActiveX(ev)
+ val y = getActiveY(ev)
+ val dx = x - mLastTouchX
+ val dy = y - mLastTouchY
+
+ if (!mIsDragging) {
+ // Use Pythagoras to see if drag length is larger than
+ // touch slop
+ mIsDragging = sqrt(((dx * dx) + (dy * dy)).toDouble()) >= mTouchSlop
+ }
+
+ if (mIsDragging) {
+ mListener!!.onDrag(dx, dy)
+ mLastTouchX = x
+ mLastTouchY = y
+
+ mVelocityTracker?.addMovement(ev)
+ }
+ }
+
+ MotionEvent.ACTION_CANCEL -> {
+ mActivePointerId = INVALID_POINTER_ID
+ // Recycle Velocity Tracker
+ if (null != mVelocityTracker) {
+ mVelocityTracker!!.recycle()
+ mVelocityTracker = null
+ }
+ }
+
+ MotionEvent.ACTION_UP -> {
+ mActivePointerId = INVALID_POINTER_ID
+ if (mIsDragging) {
+ if (null != mVelocityTracker) {
+ mLastTouchX = getActiveX(ev)
+ mLastTouchY = getActiveY(ev)
+
+ // Compute velocity within the last 1000ms
+ mVelocityTracker!!.addMovement(ev)
+ mVelocityTracker!!.computeCurrentVelocity(1000)
+
+ val vX = mVelocityTracker!!.xVelocity
+ val vY = mVelocityTracker!!
+ .yVelocity
+
+ // If the velocity is greater than minVelocity, call
+ // listener
+ if (max(abs(vX.toDouble()), abs(vY.toDouble())) >= mMinimumVelocity) {
+ mListener!!.onFling(
+ mLastTouchX, mLastTouchY, -vX,
+ -vY
+ )
+ }
+ }
+ }
+
+ // Recycle Velocity Tracker
+ if (null != mVelocityTracker) {
+ mVelocityTracker!!.recycle()
+ mVelocityTracker = null
+ }
+ }
+
+ MotionEvent.ACTION_POINTER_UP -> {
+ val pointerIndex: Int = PhotoUtils.getPointerIndex(ev.action)
+ val pointerId = ev.getPointerId(pointerIndex)
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ val newPointerIndex = if (pointerIndex == 0) 1 else 0
+ mActivePointerId = ev.getPointerId(newPointerIndex)
+ mLastTouchX = ev.getX(newPointerIndex)
+ mLastTouchY = ev.getY(newPointerIndex)
+ }
+ }
+ }
+
+ mActivePointerIndex = ev
+ .findPointerIndex(
+ if (mActivePointerId != INVALID_POINTER_ID)
+ mActivePointerId
+ else
+ 0
+ )
+ return true
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnGestureListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnGestureListener.kt
new file mode 100644
index 0000000..a2f0205
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnGestureListener.kt
@@ -0,0 +1,13 @@
+package com.tenlionsoft.medialib.base.widget
+
+interface OnGestureListener {
+ fun onDrag(dx: Float, dy: Float)
+
+ fun onFling(
+ startX: Float, startY: Float, velocityX: Float,
+ velocityY: Float
+ )
+
+ fun onScale(scaleFactor: Float, focusX: Float, focusY: Float)
+
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnMatrixChangedListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnMatrixChangedListener.kt
new file mode 100644
index 0000000..5564c83
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnMatrixChangedListener.kt
@@ -0,0 +1,13 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.graphics.RectF
+
+interface OnMatrixChangedListener {
+ /**
+ * Callback for when the Matrix displaying the Drawable has changed. This could be because
+ * the View's bounds have changed, or the user has zoomed.
+ *
+ * @param rect - Rectangle displaying the Drawable's new bounds.
+ */
+ fun onMatrixChanged(rect: RectF?)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnOutsidePhotoTapListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnOutsidePhotoTapListener.kt
new file mode 100644
index 0000000..5d183dc
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnOutsidePhotoTapListener.kt
@@ -0,0 +1,10 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.widget.ImageView
+
+interface OnOutsidePhotoTapListener {
+ /**
+ * The outside of the photo has been tapped
+ */
+ fun onOutsidePhotoTap(imageView: ImageView?)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnPhotoTapListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnPhotoTapListener.kt
new file mode 100644
index 0000000..7824f78
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnPhotoTapListener.kt
@@ -0,0 +1,7 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.widget.ImageView
+
+interface OnPhotoTapListener {
+ fun onPhotoTap(view: ImageView?, x: Float, y: Float)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnScaleChangedListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnScaleChangedListener.kt
new file mode 100644
index 0000000..e3cce8e
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnScaleChangedListener.kt
@@ -0,0 +1,5 @@
+package com.tenlionsoft.medialib.base.widget
+
+interface OnScaleChangedListener {
+ fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnSingleFlingListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnSingleFlingListener.kt
new file mode 100644
index 0000000..56de270
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnSingleFlingListener.kt
@@ -0,0 +1,7 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.view.MotionEvent
+
+interface OnSingleFlingListener {
+ fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewDragListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewDragListener.kt
new file mode 100644
index 0000000..d5aeacb
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewDragListener.kt
@@ -0,0 +1,5 @@
+package com.tenlionsoft.medialib.base.widget
+
+interface OnViewDragListener {
+ fun onDrag(dx: Float, dy: Float)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewTapListener.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewTapListener.kt
new file mode 100644
index 0000000..9aa2e34
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewTapListener.kt
@@ -0,0 +1,7 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.view.View
+
+interface OnViewTapListener {
+ fun onViewTap(view: View?, x: Float, y: Float)
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoUtils.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoUtils.kt
new file mode 100644
index 0000000..9a26513
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoUtils.kt
@@ -0,0 +1,35 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.view.MotionEvent
+import android.widget.ImageView
+import android.widget.ImageView.ScaleType
+
+class PhotoUtils {
+ companion object {
+ fun checkZoomLevels(
+ minZoom: Float, midZoom: Float,
+ maxZoom: Float
+ ) {
+ require(!(minZoom >= midZoom)) { "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value" }
+ require(!(midZoom >= maxZoom)) { "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value" }
+ }
+
+ fun hasDrawable(imageView: ImageView): Boolean {
+ return imageView.drawable != null
+ }
+
+ fun isSupportedScaleType(scaleType: ScaleType?): Boolean {
+ if (scaleType == null) {
+ return false
+ }
+ if (scaleType == ScaleType.MATRIX) {
+ throw IllegalStateException("Matrix scale type is not supported")
+ }
+ return true
+ }
+
+ fun getPointerIndex(action: Int): Int {
+ return (action and MotionEvent.ACTION_POINTER_INDEX_MASK) shr MotionEvent.ACTION_POINTER_INDEX_SHIFT
+ }
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoView.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoView.kt
new file mode 100644
index 0000000..d90ce59
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoView.kt
@@ -0,0 +1,222 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.content.Context
+import android.graphics.Matrix
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.GestureDetector
+import androidx.appcompat.widget.AppCompatImageView
+
+class PhotoView : AppCompatImageView {
+ private lateinit var attacher: PhotoViewAttacher
+ private var pendingScaleType: ScaleType? = null
+
+ constructor(context: Context) : this(context, null) {}
+ constructor(context: Context, attr: AttributeSet?) : this(context, null, 0) {}
+ constructor(context: Context, attr: AttributeSet?, defStyle: Int) : super(
+ context,
+ attr,
+ defStyle
+ ) {
+ init()
+ }
+
+ private fun init() {
+ attacher = PhotoViewAttacher(this)
+ //We always pose as a Matrix scale type, though we can change to another scale type
+ //via the attacher
+ super.setScaleType(ScaleType.MATRIX)
+ //apply the previously applied scale type
+ if (pendingScaleType != null) {
+ scaleType = pendingScaleType!!
+ pendingScaleType = null
+ }
+ }
+
+ /**
+ * Get the current [PhotoViewAttacher] for this view. Be wary of holding on to references
+ * to this attacher, as it has a reference to this view, which, if a reference is held in the
+ * wrong place, can cause memory leaks.
+ *
+ * @return the attacher.
+ */
+ fun getAttacher(): PhotoViewAttacher {
+ return attacher
+ }
+
+ override fun getScaleType(): ScaleType {
+ return attacher.getScaleType()
+ }
+
+ override fun getImageMatrix(): Matrix {
+ return attacher.getImageMatrix()
+ }
+
+ override fun setOnLongClickListener(l: OnLongClickListener?) {
+ attacher.setOnLongClickListener(l)
+ }
+
+ override fun setOnClickListener(l: OnClickListener?) {
+ attacher.setOnClickListener(l)
+ }
+
+ override fun setScaleType(scaleType: ScaleType) {
+ if (attacher == null) {
+ pendingScaleType = scaleType
+ } else {
+ attacher.setScaleType(scaleType)
+ }
+ }
+
+ override fun setImageDrawable(drawable: Drawable?) {
+ super.setImageDrawable(drawable)
+ // setImageBitmap calls through to this method
+ if (attacher != null) {
+ attacher.update()
+ }
+ }
+
+ override fun setImageResource(resId: Int) {
+ super.setImageResource(resId)
+ if (attacher != null) {
+ attacher.update()
+ }
+ }
+
+ override fun setImageURI(uri: Uri?) {
+ super.setImageURI(uri)
+ if (attacher != null) {
+ attacher.update()
+ }
+ }
+
+ override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
+ val changed = super.setFrame(l, t, r, b)
+ if (changed) {
+ attacher.update()
+ }
+ return changed
+ }
+
+ fun setRotationTo(rotationDegree: Float) {
+ attacher.setRotationTo(rotationDegree)
+ }
+
+ fun setRotationBy(rotationDegree: Float) {
+ attacher.setRotationBy(rotationDegree)
+ }
+
+ fun isZoomable(): Boolean {
+ return attacher.isZoomable()
+ }
+
+ fun setZoomable(zoomable: Boolean) {
+ attacher.setZoomable(zoomable)
+ }
+
+ fun getDisplayRect(): RectF {
+ return attacher.getDisplayRect()!!
+ }
+
+ fun getDisplayMatrix(matrix: Matrix?) {
+ attacher.getDisplayMatrix(matrix!!)
+ }
+
+ fun setDisplayMatrix(finalRectangle: Matrix?): Boolean {
+ return attacher.setDisplayMatrix(finalRectangle)
+ }
+
+ fun getSuppMatrix(matrix: Matrix?) {
+ attacher.getSuppMatrix(matrix!!)
+ }
+
+ fun setSuppMatrix(matrix: Matrix?): Boolean {
+ return attacher.setDisplayMatrix(matrix)
+ }
+
+ fun getMinimumScale(): Float {
+ return attacher.getMinimumScale()
+ }
+
+ fun getMediumScale(): Float {
+ return attacher.getMediumScale()
+ }
+
+ fun getMaximumScale(): Float {
+ return attacher.getMaximumScale()
+ }
+
+ fun getScale(): Float {
+ return attacher.getScale()
+ }
+
+ fun setAllowParentInterceptOnEdge(allow: Boolean) {
+ attacher.setAllowParentInterceptOnEdge(allow)
+ }
+
+ fun setMinimumScale(minimumScale: Float) {
+ attacher.setMinimumScale(minimumScale)
+ }
+
+ fun setMediumScale(mediumScale: Float) {
+ attacher.setMediumScale(mediumScale)
+ }
+
+ fun setMaximumScale(maximumScale: Float) {
+ attacher.setMaximumScale(maximumScale)
+ }
+
+ fun setScaleLevels(minimumScale: Float, mediumScale: Float, maximumScale: Float) {
+ attacher.setScaleLevels(minimumScale, mediumScale, maximumScale)
+ }
+
+ fun setOnMatrixChangeListener(listener: OnMatrixChangedListener) {
+ attacher.setOnMatrixChangeListener(listener)
+ }
+
+ fun setOnPhotoTapListener(listener: OnPhotoTapListener) {
+ attacher.setOnPhotoTapListener(listener)
+ }
+
+ fun setOnOutsidePhotoTapListener(listener: OnOutsidePhotoTapListener?) {
+ attacher.setOnOutsidePhotoTapListener(listener)
+ }
+
+ fun setOnViewTapListener(listener: OnViewTapListener) {
+ attacher.setOnViewTapListener(listener)
+ }
+
+ fun setOnViewDragListener(listener: OnViewDragListener) {
+ attacher.setOnViewDragListener(listener)
+ }
+
+ fun setScale(scale: Float) {
+ attacher.setScale(scale)
+ }
+
+ fun setScale(scale: Float, animate: Boolean) {
+ attacher.setScale(scale, animate)
+ }
+
+ fun setScale(scale: Float, focalX: Float, focalY: Float, animate: Boolean) {
+ attacher.setScale(scale, focalX, focalY, animate)
+ }
+
+ fun setZoomTransitionDuration(milliseconds: Int) {
+ attacher.setZoomTransitionDuration(milliseconds)
+ }
+
+ fun setOnDoubleTapListener(onDoubleTapListener: GestureDetector.OnDoubleTapListener?) {
+ attacher.setOnDoubleTapListener(onDoubleTapListener)
+ }
+
+ fun setOnScaleChangeListener(onScaleChangedListener: OnScaleChangedListener?) {
+ attacher.setOnScaleChangeListener(onScaleChangedListener)
+ }
+
+ fun setOnSingleFlingListener(onSingleFlingListener: OnSingleFlingListener?) {
+ attacher.setOnSingleFlingListener(onSingleFlingListener)
+ }
+}
\ No newline at end of file
diff --git a/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoViewAttacher.kt b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoViewAttacher.kt
new file mode 100644
index 0000000..94cffe3
--- /dev/null
+++ b/medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoViewAttacher.kt
@@ -0,0 +1,779 @@
+package com.tenlionsoft.medialib.base.widget
+
+import android.content.Context
+import android.graphics.Matrix
+import android.graphics.Matrix.ScaleToFit
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.view.animation.Interpolator
+import android.widget.ImageView
+import android.widget.ImageView.ScaleType
+import android.widget.OverScroller
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+class PhotoViewAttacher : View.OnTouchListener, View.OnLayoutChangeListener {
+ var DEFAULT_MAX_SCALE: Float = 3.0f
+ var DEFAULT_MID_SCALE: Float = 1.75f
+ var DEFAULT_MIN_SCALE: Float = 1.0f
+ var DEFAULT_ZOOM_DURATION: Int = 200
+
+ val EDGE_NONE: Int = -1
+ val EDGE_LEFT: Int = 0
+ val EDGE_RIGHT: Int = 1
+ val EDGE_BOTH: Int = 2
+ var SINGLE_TOUCH: Int = 1
+
+ private var mInterpolator: Interpolator = AccelerateDecelerateInterpolator()
+ private var mZoomDuration = DEFAULT_ZOOM_DURATION
+ private var mMinScale = DEFAULT_MIN_SCALE
+ private var mMidScale = DEFAULT_MID_SCALE
+ private var mMaxScale = DEFAULT_MAX_SCALE
+
+ private var mAllowParentInterceptOnEdge = true
+ private var mBlockParentIntercept = false
+
+ private var mImageView: ImageView? = null
+
+ // Gesture Detectors
+ private lateinit var mGestureDetector: GestureDetector
+ private lateinit var mScaleDragDetector: CustomGestureDetector
+
+ // These are set so we don't keep allocating them on the heap
+ private val mBaseMatrix = Matrix()
+ private val mDrawMatrix = Matrix()
+ private val mSuppMatrix = Matrix()
+ private val mDisplayRect = RectF()
+ private val mMatrixValues = FloatArray(9)
+
+ // Listeners
+ private var mMatrixChangeListener: OnMatrixChangedListener? = null
+ private var mPhotoTapListener: OnPhotoTapListener? = null
+ private var mOutsidePhotoTapListener: OnOutsidePhotoTapListener? = null
+ private var mViewTapListener: OnViewTapListener? = null
+ private var mOnClickListener: View.OnClickListener? = null
+ private var mLongClickListener: OnLongClickListener? = null
+ private var mScaleChangeListener: OnScaleChangedListener? = null
+ private var mSingleFlingListener: OnSingleFlingListener? = null
+ private var mOnViewDragListener: OnViewDragListener? = null
+
+ private var mCurrentFlingRunnable: PhotoViewAttacher.FlingRunnable? =
+ null
+ private var mScrollEdge = EDGE_BOTH
+ private var mBaseRotation = 0f
+
+ private var mZoomEnabled = true
+ private var mScaleType = ScaleType.FIT_CENTER
+
+
+ private val onGestureListener: OnGestureListener = object : OnGestureListener {
+ override fun onDrag(dx: Float, dy: Float) {
+ if (mScaleDragDetector.isScaling()) {
+ return // Do not drag if we are already scaling
+ }
+ mOnViewDragListener?.onDrag(dx, dy)
+ mSuppMatrix.postTranslate(dx, dy)
+ checkAndDisplayMatrix()
+
+ /*
+ * Here we decide whether to let the ImageView's parent to start taking
+ * over the touch event.
+ *
+ * First we check whether this function is enabled. We never want the
+ * parent to take over if we're scaling. We then check the edge we're
+ * on, and the direction of the scroll (i.e. if we're pulling against
+ * the edge, aka 'overscrolling', let the parent take over).
+ */
+ val parent = mImageView!!.parent
+ if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
+ if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f)
+ || (mScrollEdge == EDGE_RIGHT && dx <= -1f)
+ ) {
+ parent?.requestDisallowInterceptTouchEvent(false)
+ }
+ } else {
+ parent?.requestDisallowInterceptTouchEvent(true)
+ }
+ }
+
+ override fun onFling(startX: Float, startY: Float, velocityX: Float, velocityY: Float) {
+ mCurrentFlingRunnable = FlingRunnable(mImageView!!.context)
+ mCurrentFlingRunnable!!.fling(
+ getImageViewWidth(mImageView!!),
+ getImageViewHeight(mImageView!!), velocityX.toInt(), velocityY.toInt()
+ )
+ mImageView?.post(mCurrentFlingRunnable)
+ }
+
+ override fun onScale(scaleFactor: Float, focusX: Float, focusY: Float) {
+ if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
+ mScaleChangeListener?.onScaleChange(scaleFactor, focusX, focusY)
+ mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY)
+ checkAndDisplayMatrix()
+ }
+ }
+ }
+
+ constructor(imageView: ImageView) {
+
+ mImageView = imageView
+ imageView.setOnTouchListener(this)
+ imageView.addOnLayoutChangeListener(this)
+ if (imageView.isInEditMode) {
+ return
+ }
+ mBaseRotation = 0.0f
+ // Create Gesture Detectors...
+ mScaleDragDetector = CustomGestureDetector(imageView.context, onGestureListener)
+ mGestureDetector = GestureDetector(imageView.context, object : SimpleOnGestureListener() {
+ // forward long click listener
+ override fun onLongPress(e: MotionEvent) {
+ mLongClickListener?.onLongClick(mImageView)
+ }
+
+ override fun onFling(
+ e1: MotionEvent?, e2: MotionEvent,
+ velocityX: Float, velocityY: Float
+ ): Boolean {
+ if (mSingleFlingListener != null) {
+ if (getScale() > DEFAULT_MIN_SCALE) {
+ return false
+ }
+ if (e1!!.pointerCount > SINGLE_TOUCH
+ || e2.pointerCount > SINGLE_TOUCH
+ ) {
+ return false
+ }
+ return mSingleFlingListener!!.onFling(e1, e2, velocityX, velocityY)
+ }
+ return false
+ }
+ })
+ mGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ mOnClickListener?.onClick(mImageView)
+ val displayRect = getDisplayRect()
+ val x = e.x
+ val y = e.y
+ mViewTapListener?.onViewTap(mImageView, x, y)
+ if (displayRect != null) {
+ // Check to see if the user tapped on the photo
+ if (displayRect.contains(x, y)) {
+ val xResult = ((x - displayRect.left)
+ / displayRect.width())
+ val yResult = ((y - displayRect.top)
+ / displayRect.height())
+ mPhotoTapListener?.onPhotoTap(mImageView, xResult, yResult)
+ return true
+ } else {
+ mOutsidePhotoTapListener?.onOutsidePhotoTap(mImageView)
+ }
+ }
+ return false
+ }
+
+ override fun onDoubleTap(ev: MotionEvent): Boolean {
+ try {
+ val scale = getScale()
+ val x = ev.x
+ val y = ev.y
+ if (scale < getMediumScale()) {
+ setScale(getMediumScale(), x, y, true)
+ } else if (scale >= getMediumScale() && scale < getMaximumScale()) {
+ setScale(getMaximumScale(), x, y, true)
+ } else {
+ setScale(getMinimumScale(), x, y, true)
+ }
+ } catch (e: ArrayIndexOutOfBoundsException) {
+ // Can sometimes happen when getX() and getY() is called
+ }
+ return true
+ }
+
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+ // Wait for the confirmed onDoubleTap() instead
+ return false
+ }
+ })
+ }
+
+
+ fun setOnDoubleTapListener(newOnDoubleTapListener: GestureDetector.OnDoubleTapListener?) {
+ mGestureDetector!!.setOnDoubleTapListener(newOnDoubleTapListener)
+ }
+
+ fun setOnScaleChangeListener(onScaleChangeListener: OnScaleChangedListener?) {
+ this.mScaleChangeListener = onScaleChangeListener
+ }
+
+ fun setOnSingleFlingListener(onSingleFlingListener: OnSingleFlingListener?) {
+ this.mSingleFlingListener = onSingleFlingListener
+ }
+
+ @Deprecated("")
+ fun isZoomEnabled(): Boolean {
+ return mZoomEnabled
+ }
+
+ fun getDisplayRect(): RectF? {
+ checkMatrixBounds()
+ return getDisplayRect(getDrawMatrix())
+ }
+
+ fun setDisplayMatrix(finalMatrix: Matrix?): Boolean {
+ requireNotNull(finalMatrix) { "Matrix cannot be null" }
+ if (mImageView!!.drawable == null) {
+ return false
+ }
+ mSuppMatrix.set(finalMatrix)
+ checkAndDisplayMatrix()
+ return true
+ }
+
+ fun setBaseRotation(degrees: Float) {
+ mBaseRotation = degrees % 360
+ update()
+ setRotationBy(mBaseRotation)
+ checkAndDisplayMatrix()
+ }
+
+ fun setRotationTo(degrees: Float) {
+ mSuppMatrix.setRotate(degrees % 360)
+ checkAndDisplayMatrix()
+ }
+
+ fun setRotationBy(degrees: Float) {
+ mSuppMatrix.postRotate(degrees % 360)
+ checkAndDisplayMatrix()
+ }
+
+ fun getMinimumScale(): Float {
+ return mMinScale
+ }
+
+ fun getMediumScale(): Float {
+ return mMidScale
+ }
+
+ fun getMaximumScale(): Float {
+ return mMaxScale
+ }
+
+ fun getScale(): Float {
+ return sqrt(
+ (getValue(
+ mSuppMatrix,
+ Matrix.MSCALE_X
+ ).pow(2.0F) + getValue(
+ mSuppMatrix,
+ Matrix.MSKEW_Y
+ ).pow(2.0F)).toDouble()
+ ).toFloat()
+ }
+
+ fun getScaleType(): ScaleType {
+ return mScaleType
+ }
+
+ override fun onLayoutChange(
+ v: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ // Update our base matrix, as the bounds have changed
+ if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
+ updateBaseMatrix(mImageView!!.drawable)
+ }
+ }
+
+ override fun onTouch(v: View, ev: MotionEvent): Boolean {
+ var handled = false
+ if (mZoomEnabled && PhotoUtils.hasDrawable(v as ImageView)) {
+ when (ev.action) {
+ MotionEvent.ACTION_DOWN -> {
+ val parent = v.getParent()
+ // First, disable the Parent from intercepting the touch
+ // event
+ parent?.requestDisallowInterceptTouchEvent(true)
+ // If we're flinging, and the user presses down, cancel
+ // fling
+ cancelFling()
+ }
+
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> // If the user has zoomed less than min scale, zoom back
+ // to min scale
+ if (getScale() < mMinScale) {
+ val rect = getDisplayRect()
+ if (rect != null) {
+ v.post(
+ AnimatedZoomRunnable(
+ getScale(), mMinScale,
+ rect.centerX(), rect.centerY()
+ )
+ )
+ handled = true
+ }
+ } else if (getScale() > mMaxScale) {
+ val rect = getDisplayRect()
+ if (rect != null) {
+ v.post(
+ AnimatedZoomRunnable(
+ getScale(), mMaxScale,
+ rect.centerX(), rect.centerY()
+ )
+ )
+ handled = true
+ }
+ }
+ }
+ // Try the Scale/Drag detector
+ if (mScaleDragDetector != null) {
+ val wasScaling = mScaleDragDetector.isScaling()
+ val wasDragging = mScaleDragDetector.isDragging()
+ handled = mScaleDragDetector.onTouchEvent(ev)
+ val didntScale = !wasScaling && !mScaleDragDetector.isScaling()
+ val didntDrag = !wasDragging && !mScaleDragDetector.isDragging()
+ mBlockParentIntercept = didntScale && didntDrag
+ }
+ // Check to see if the user double tapped
+ if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
+ handled = true
+ }
+ }
+ return handled
+ }
+
+ fun setAllowParentInterceptOnEdge(allow: Boolean) {
+ mAllowParentInterceptOnEdge = allow
+ }
+
+ fun setMinimumScale(minimumScale: Float) {
+ PhotoUtils.checkZoomLevels(minimumScale, mMidScale, mMaxScale)
+ mMinScale = minimumScale
+ }
+
+ fun setMediumScale(mediumScale: Float) {
+ PhotoUtils.checkZoomLevels(mMinScale, mediumScale, mMaxScale)
+ mMidScale = mediumScale
+ }
+
+ fun setMaximumScale(maximumScale: Float) {
+ PhotoUtils.checkZoomLevels(mMinScale, mMidScale, maximumScale)
+ mMaxScale = maximumScale
+ }
+
+ fun setScaleLevels(minimumScale: Float, mediumScale: Float, maximumScale: Float) {
+ PhotoUtils.checkZoomLevels(minimumScale, mediumScale, maximumScale)
+ mMinScale = minimumScale
+ mMidScale = mediumScale
+ mMaxScale = maximumScale
+ }
+
+ fun setOnLongClickListener(listener: OnLongClickListener?) {
+ mLongClickListener = listener
+ }
+
+ fun setOnClickListener(listener: View.OnClickListener?) {
+ mOnClickListener = listener
+ }
+
+ fun setOnMatrixChangeListener(listener: OnMatrixChangedListener) {
+ mMatrixChangeListener = listener
+ }
+
+ fun setOnPhotoTapListener(listener: OnPhotoTapListener) {
+ mPhotoTapListener = listener
+ }
+
+ fun setOnOutsidePhotoTapListener(mOutsidePhotoTapListener: OnOutsidePhotoTapListener?) {
+ this.mOutsidePhotoTapListener = mOutsidePhotoTapListener
+ }
+
+ fun setOnViewTapListener(listener: OnViewTapListener) {
+ mViewTapListener = listener
+ }
+
+ fun setOnViewDragListener(listener: OnViewDragListener) {
+ mOnViewDragListener = listener
+ }
+
+ fun setScale(scale: Float) {
+ setScale(scale, false)
+ }
+
+ fun setScale(scale: Float, animate: Boolean) {
+ setScale(
+ scale,
+ ((mImageView!!.right) / 2).toFloat(),
+ ((mImageView!!.bottom) / 2).toFloat(),
+ animate
+ )
+ }
+
+ fun setScale(
+ scale: Float, focalX: Float, focalY: Float,
+ animate: Boolean
+ ) {
+ // Check to see if the scale is within bounds
+ require(!(scale < mMinScale || scale > mMaxScale)) { "Scale must be within the range of minScale and maxScale" }
+ if (animate) {
+ mImageView!!.post(
+ AnimatedZoomRunnable(
+ getScale(), scale,
+ focalX, focalY
+ )
+ )
+ } else {
+ mSuppMatrix.setScale(scale, scale, focalX, focalY)
+ checkAndDisplayMatrix()
+ }
+ }
+
+ /**
+ * Set the zoom interpolator
+ *
+ * @param interpolator the zoom interpolator
+ */
+ fun setZoomInterpolator(interpolator: Interpolator) {
+ mInterpolator = interpolator
+ }
+
+ fun setScaleType(scaleType: ScaleType) {
+ if (PhotoUtils.isSupportedScaleType(scaleType) && scaleType != mScaleType) {
+ mScaleType = scaleType
+ update()
+ }
+ }
+
+ fun isZoomable(): Boolean {
+ return mZoomEnabled
+ }
+
+ fun setZoomable(zoomable: Boolean) {
+ mZoomEnabled = zoomable
+ update()
+ }
+
+ fun update() {
+ if (mZoomEnabled) {
+ // Update the base matrix using the current drawable
+ updateBaseMatrix(mImageView!!.drawable)
+ } else {
+ // Reset the Matrix...
+ resetMatrix()
+ }
+ }
+
+ /**
+ * Get the display matrix
+ *
+ * @param matrix target matrix to copy to
+ */
+ fun getDisplayMatrix(matrix: Matrix) {
+ matrix.set(getDrawMatrix())
+ }
+
+ /**
+ * Get the current support matrix
+ */
+ fun getSuppMatrix(matrix: Matrix) {
+ matrix.set(mSuppMatrix)
+ }
+
+ private fun getDrawMatrix(): Matrix {
+ mDrawMatrix.set(mBaseMatrix)
+ mDrawMatrix.postConcat(mSuppMatrix)
+ return mDrawMatrix
+ }
+
+ fun getImageMatrix(): Matrix {
+ return mDrawMatrix
+ }
+
+ fun setZoomTransitionDuration(milliseconds: Int) {
+ this.mZoomDuration = milliseconds
+ }
+
+ /**
+ * Helper method that 'unpacks' a Matrix and returns the required value
+ *
+ * @param matrix Matrix to unpack
+ * @param whichValue Which value from Matrix.M* to return
+ * @return returned value
+ */
+ private fun getValue(matrix: Matrix, whichValue: Int): Float {
+ matrix.getValues(mMatrixValues)
+ return mMatrixValues[whichValue]
+ }
+
+ /**
+ * Resets the Matrix back to FIT_CENTER, and then displays its contents
+ */
+ private fun resetMatrix() {
+ mSuppMatrix.reset()
+ setRotationBy(mBaseRotation)
+ setImageViewMatrix(getDrawMatrix())
+ checkMatrixBounds()
+ }
+
+ private fun setImageViewMatrix(matrix: Matrix) {
+ mImageView!!.imageMatrix = matrix
+ // Call MatrixChangedListener if needed
+ if (mMatrixChangeListener != null) {
+ val displayRect = getDisplayRect(matrix)
+ if (displayRect != null) {
+ mMatrixChangeListener!!.onMatrixChanged(displayRect)
+ }
+ }
+ }
+
+ /**
+ * Helper method that simply checks the Matrix, and then displays the result
+ */
+ private fun checkAndDisplayMatrix() {
+ if (checkMatrixBounds()) {
+ setImageViewMatrix(getDrawMatrix())
+ }
+ }
+
+ /**
+ * Helper method that maps the supplied Matrix to the current Drawable
+ *
+ * @param matrix - Matrix to map Drawable against
+ * @return RectF - Displayed Rectangle
+ */
+ private fun getDisplayRect(matrix: Matrix): RectF? {
+ val d = mImageView!!.drawable
+ if (d != null) {
+ mDisplayRect[0f, 0f, d.intrinsicWidth.toFloat()] = d.intrinsicHeight.toFloat()
+ matrix.mapRect(mDisplayRect)
+ return mDisplayRect
+ }
+ return null
+ }
+
+ /**
+ * Calculate Matrix for FIT_CENTER
+ *
+ * @param drawable - Drawable being displayed
+ */
+ private fun updateBaseMatrix(drawable: Drawable?) {
+ if (drawable == null) {
+ return
+ }
+ val viewWidth = getImageViewWidth(mImageView!!).toFloat()
+ val viewHeight = getImageViewHeight(mImageView!!).toFloat()
+ val drawableWidth = drawable.intrinsicWidth
+ val drawableHeight = drawable.intrinsicHeight
+ mBaseMatrix.reset()
+ val widthScale = viewWidth / drawableWidth
+ val heightScale = viewHeight / drawableHeight
+ if (mScaleType == ScaleType.CENTER) {
+ mBaseMatrix.postTranslate(
+ (viewWidth - drawableWidth) / 2f,
+ (viewHeight - drawableHeight) / 2f
+ )
+ } else if (mScaleType == ScaleType.CENTER_CROP) {
+ val scale =
+ max(widthScale.toDouble(), heightScale.toDouble()).toFloat()
+ mBaseMatrix.postScale(scale, scale)
+ mBaseMatrix.postTranslate(
+ (viewWidth - drawableWidth * scale) / 2f,
+ (viewHeight - drawableHeight * scale) / 2f
+ )
+ } else if (mScaleType == ScaleType.CENTER_INSIDE) {
+ val scale =
+ min(1.0, min(widthScale.toDouble(), heightScale.toDouble())).toFloat()
+ mBaseMatrix.postScale(scale, scale)
+ mBaseMatrix.postTranslate(
+ (viewWidth - drawableWidth * scale) / 2f,
+ (viewHeight - drawableHeight * scale) / 2f
+ )
+ } else {
+ var mTempSrc = RectF(0f, 0f, drawableWidth.toFloat(), drawableHeight.toFloat())
+ val mTempDst = RectF(0f, 0f, viewWidth, viewHeight)
+ if (mBaseRotation.toInt() % 180 != 0) {
+ mTempSrc = RectF(0f, 0f, drawableHeight.toFloat(), drawableWidth.toFloat())
+ }
+ when (mScaleType) {
+ ScaleType.FIT_CENTER -> mBaseMatrix.setRectToRect(
+ mTempSrc,
+ mTempDst,
+ ScaleToFit.CENTER
+ )
+
+ ScaleType.FIT_START -> mBaseMatrix.setRectToRect(
+ mTempSrc,
+ mTempDst,
+ ScaleToFit.START
+ )
+
+ ScaleType.FIT_END -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END)
+ ScaleType.FIT_XY -> mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL)
+ else -> {}
+ }
+ }
+ resetMatrix()
+ }
+
+ private fun checkMatrixBounds(): Boolean {
+ val rect = getDisplayRect(getDrawMatrix()) ?: return false
+ val height = rect.height()
+ val width = rect.width()
+ var deltaX = 0f
+ var deltaY = 0f
+ val viewHeight = getImageViewHeight(mImageView!!)
+ if (height <= viewHeight) {
+ deltaY = when (mScaleType) {
+ ScaleType.FIT_START -> -rect.top
+ ScaleType.FIT_END -> viewHeight - height - rect.top
+ else -> (viewHeight - height) / 2 - rect.top
+ }
+ } else if (rect.top > 0) {
+ deltaY = -rect.top
+ } else if (rect.bottom < viewHeight) {
+ deltaY = viewHeight - rect.bottom
+ }
+ val viewWidth = getImageViewWidth(mImageView!!)
+ if (width <= viewWidth) {
+ deltaX = when (mScaleType) {
+ ScaleType.FIT_START -> -rect.left
+ ScaleType.FIT_END -> viewWidth - width - rect.left
+ else -> (viewWidth - width) / 2 - rect.left
+ }
+ mScrollEdge = EDGE_BOTH
+ } else if (rect.left > 0) {
+ mScrollEdge = EDGE_LEFT
+ deltaX = -rect.left
+ } else if (rect.right < viewWidth) {
+ deltaX = viewWidth - rect.right
+ mScrollEdge = EDGE_RIGHT
+ } else {
+ mScrollEdge = EDGE_NONE
+ }
+ // Finally actually translate the matrix
+ mSuppMatrix.postTranslate(deltaX, deltaY)
+ return true
+ }
+
+ private fun getImageViewWidth(imageView: ImageView): Int {
+ return imageView.width - imageView.paddingLeft - imageView.paddingRight
+ }
+
+ private fun getImageViewHeight(imageView: ImageView): Int {
+ return imageView.height - imageView.paddingTop - imageView.paddingBottom
+ }
+
+ private fun cancelFling() {
+ if (mCurrentFlingRunnable != null) {
+ mCurrentFlingRunnable!!.cancelFling()
+ mCurrentFlingRunnable = null
+ }
+ }
+
+ inner class AnimatedZoomRunnable(
+ private val mZoomStart: Float, private val mZoomEnd: Float,
+ private val mFocalX: Float, private val mFocalY: Float
+ ) : Runnable {
+ private val mStartTime = System.currentTimeMillis()
+
+ override fun run() {
+ val t = interpolate()
+ val scale = mZoomStart + t * (mZoomEnd - mZoomStart)
+ val deltaScale: Float = scale / getScale()
+ onGestureListener.onScale(deltaScale, mFocalX, mFocalY)
+ // We haven't hit our target scale yet, so post ourselves again
+ if (t < 1f) {
+ Compat.postOnAnimation(mImageView!!, this)
+ }
+ }
+
+ fun interpolate(): Float {
+ var t: Float = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration
+ t = min(1.0, t.toDouble()).toFloat()
+ t = mInterpolator.getInterpolation(t)
+ return t
+ }
+ }
+
+ inner class FlingRunnable(context: Context?) : Runnable {
+ private val mScroller = OverScroller(context)
+ private var mCurrentX = 0
+ private var mCurrentY = 0
+
+ fun cancelFling() {
+ mScroller.forceFinished(true)
+ }
+
+ fun fling(
+ viewWidth: Int, viewHeight: Int, velocityX: Int,
+ velocityY: Int
+ ) {
+ val rect: RectF = getDisplayRect() ?: return
+ val startX = Math.round(-rect.left)
+ val minX: Int
+ val maxX: Int
+ val minY: Int
+ val maxY: Int
+ if (viewWidth < rect.width()) {
+ minX = 0
+ maxX = Math.round(rect.width() - viewWidth)
+ } else {
+ maxX = startX
+ minX = maxX
+ }
+ val startY = Math.round(-rect.top)
+ if (viewHeight < rect.height()) {
+ minY = 0
+ maxY = Math.round(rect.height() - viewHeight)
+ } else {
+ maxY = startY
+ minY = maxY
+ }
+ mCurrentX = startX
+ mCurrentY = startY
+ // If we actually can move, fling the scroller
+ if (startX != maxX || startY != maxY) {
+ mScroller.fling(
+ startX, startY, velocityX, velocityY, minX,
+ maxX, minY, maxY, 0, 0
+ )
+ }
+ }
+
+ override fun run() {
+ if (mScroller.isFinished) {
+ return // remaining post that should not be handled
+ }
+ if (mScroller.computeScrollOffset()) {
+ val newX = mScroller.currX
+ val newY = mScroller.currY
+ mSuppMatrix.postTranslate(
+ (mCurrentX - newX).toFloat(),
+ (mCurrentY - newY).toFloat()
+ )
+ checkAndDisplayMatrix()
+ mCurrentX = newX
+ mCurrentY = newY
+ // Post On animation
+ Compat.postOnAnimation(mImageView!!, this)
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/medialib/src/main/res/layout/activity_simple_photo.xml b/medialib/src/main/res/layout/activity_simple_photo.xml
new file mode 100644
index 0000000..5c3b4a2
--- /dev/null
+++ b/medialib/src/main/res/layout/activity_simple_photo.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/medialib/src/main/res/layout/activity_simple_video.xml b/medialib/src/main/res/layout/activity_simple_video.xml
new file mode 100644
index 0000000..3fc671b
--- /dev/null
+++ b/medialib/src/main/res/layout/activity_simple_video.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/medialib/src/main/res/layout/item_photo.xml b/medialib/src/main/res/layout/item_photo.xml
new file mode 100644
index 0000000..7b5fc75
--- /dev/null
+++ b/medialib/src/main/res/layout/item_photo.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/medialib/src/main/res/values/attrs.xml b/medialib/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..6612afd
--- /dev/null
+++ b/medialib/src/main/res/values/attrs.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/medialib/src/main/res/values/colors.xml b/medialib/src/main/res/values/colors.xml
new file mode 100644
index 0000000..810ee2f
--- /dev/null
+++ b/medialib/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #9F1512
+ #999F1512
+ #4D000000
+
diff --git a/medialib/src/main/res/values/dimens.xml b/medialib/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..0c30e6a
--- /dev/null
+++ b/medialib/src/main/res/values/dimens.xml
@@ -0,0 +1,12 @@
+
+
+ 46dp
+ 12dp
+ 16sp
+ 10dp
+ 14dp
+ 14dp
+ 1dp
+ 50dp
+ 14sp
+
\ No newline at end of file
diff --git a/medialib/src/main/res/values/strings.xml b/medialib/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9149d98
--- /dev/null
+++ b/medialib/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ 重新播放
+ 请先解锁屏幕!
+ 已解锁
+ 已锁定
+ 出了点小问题,请稍后重试
+ 重 试
+ 继续播放
+ 您正在使用移动网络,继续播放将消耗流量
+ %1$d / %2$d
+
diff --git a/medialib/src/test/java/com/tenlionsoft/medialib/ExampleUnitTest.java b/medialib/src/test/java/com/tenlionsoft/medialib/ExampleUnitTest.java
new file mode 100644
index 0000000..037341e
--- /dev/null
+++ b/medialib/src/test/java/com/tenlionsoft/medialib/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.tenlionsoft.medialib;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f7cd185..01a4378 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,9 +17,11 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven(url = "https://jitpack.io")
+ maven(url = "https://maven.aliyun.com/repository/public")
}
}
rootProject.name = "aimz_k"
include(":app")
include(":baselib")
+include(":medialib")