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")