From 06f10d3ed3c2cedcae352da1a2ea6292327901d6 Mon Sep 17 00:00:00 2001 From: itgaojian163 Date: Tue, 5 Nov 2024 17:49:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=84=E8=A7=88=E5=9B=BE=E7=89=87=E3=80=81?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E3=80=81=E5=88=A0=E9=99=A4=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E3=80=81=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5=E9=93=83=E5=A3=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 2 +- .idea/misc.xml | 3 +- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 18 +- .../aimz_k/adapter/CategoryAdapter.kt | 20 +- .../aimz_k/model/MsgCategoryDao.kt | 4 + .../com/tenlionsoft/aimz_k/model/MsgDao.kt | 4 +- .../aimz_k/page/activity/ChatActivity.kt | 104 ++- .../aimz_k/page/fragments/MsgFragment.kt | 50 +- .../aimz_k/services/SocketService.kt | 9 +- .../aimz_k/viewmodel/ChatPageViewModel.kt | 3 +- .../aimz_k/viewmodel/MsgViewModel.kt | 23 +- app/src/main/res/values/themes.xml | 12 + baselib/build.gradle.kts | 2 +- .../widget/AdapterItemLongClickListener.kt | 5 + baselib/src/main/res/anim/fade_in.xml | 6 + baselib/src/main/res/anim/fade_out.xml | 6 + baselib/src/main/res/anim/slide_in_right.xml | 7 + baselib/src/main/res/anim/slide_out_left.xml | 7 + .../src/main/res/drawable-xhdpi/ic_del.png | Bin 0 -> 808 bytes .../res/drawable-xhdpi/ic_img_loading.png | Bin 0 -> 6429 bytes .../src/main/res/layout/dialog_loading.xml | 28 +- baselib/src/main/res/raw/chat.mp3 | Bin 0 -> 58514 bytes gradle/libs.versions.toml | 12 +- medialib/.gitignore | 1 + medialib/build.gradle.kts | 60 ++ medialib/consumer-rules.pro | 0 medialib/proguard-rules.pro | 21 + .../medialib/ExampleInstrumentedTest.java | 26 + medialib/src/main/AndroidManifest.xml | 15 + .../medialib/base/SimplePhotoActivity.kt | 61 ++ .../medialib/base/SimpleVideoActivity.kt | 96 +++ .../medialib/base/adapter/PhotoAdapter.kt | 57 ++ .../medialib/base/widget/Compat.kt | 17 + .../base/widget/CustomGestureDetector.kt | 193 +++++ .../medialib/base/widget/OnGestureListener.kt | 13 + .../base/widget/OnMatrixChangedListener.kt | 13 + .../base/widget/OnOutsidePhotoTapListener.kt | 10 + .../base/widget/OnPhotoTapListener.kt | 7 + .../base/widget/OnScaleChangedListener.kt | 5 + .../base/widget/OnSingleFlingListener.kt | 7 + .../base/widget/OnViewDragListener.kt | 5 + .../medialib/base/widget/OnViewTapListener.kt | 7 + .../medialib/base/widget/PhotoUtils.kt | 35 + .../medialib/base/widget/PhotoView.kt | 222 +++++ .../medialib/base/widget/PhotoViewAttacher.kt | 779 ++++++++++++++++++ .../main/res/layout/activity_simple_photo.xml | 32 + .../main/res/layout/activity_simple_video.xml | 21 + medialib/src/main/res/layout/item_photo.xml | 11 + medialib/src/main/res/values/attrs.xml | 16 + medialib/src/main/res/values/colors.xml | 6 + medialib/src/main/res/values/dimens.xml | 12 + medialib/src/main/res/values/strings.xml | 11 + .../tenlionsoft/medialib/ExampleUnitTest.java | 17 + settings.gradle.kts | 2 + 55 files changed, 2017 insertions(+), 88 deletions(-) create mode 100644 baselib/src/main/java/com/tenlionsoft/baselib/widget/AdapterItemLongClickListener.kt create mode 100644 baselib/src/main/res/anim/fade_in.xml create mode 100644 baselib/src/main/res/anim/fade_out.xml create mode 100644 baselib/src/main/res/anim/slide_in_right.xml create mode 100644 baselib/src/main/res/anim/slide_out_left.xml create mode 100644 baselib/src/main/res/drawable-xhdpi/ic_del.png create mode 100644 baselib/src/main/res/drawable-xhdpi/ic_img_loading.png create mode 100644 baselib/src/main/res/raw/chat.mp3 create mode 100644 medialib/.gitignore create mode 100644 medialib/build.gradle.kts create mode 100644 medialib/consumer-rules.pro create mode 100644 medialib/proguard-rules.pro create mode 100644 medialib/src/androidTest/java/com/tenlionsoft/medialib/ExampleInstrumentedTest.java create mode 100644 medialib/src/main/AndroidManifest.xml create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/SimplePhotoActivity.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/SimpleVideoActivity.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/adapter/PhotoAdapter.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/Compat.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/CustomGestureDetector.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnGestureListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnMatrixChangedListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnOutsidePhotoTapListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnPhotoTapListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnScaleChangedListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnSingleFlingListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewDragListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/OnViewTapListener.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoUtils.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoView.kt create mode 100644 medialib/src/main/java/com/tenlionsoft/medialib/base/widget/PhotoViewAttacher.kt create mode 100644 medialib/src/main/res/layout/activity_simple_photo.xml create mode 100644 medialib/src/main/res/layout/activity_simple_video.xml create mode 100644 medialib/src/main/res/layout/item_photo.xml create mode 100644 medialib/src/main/res/values/attrs.xml create mode 100644 medialib/src/main/res/values/colors.xml create mode 100644 medialib/src/main/res/values/dimens.xml create mode 100644 medialib/src/main/res/values/strings.xml create mode 100644 medialib/src/test/java/com/tenlionsoft/medialib/ExampleUnitTest.java 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 0000000000000000000000000000000000000000..d882852a4e6a4119202df7e1c6ebbe70d5ba37a3 GIT binary patch literal 808 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(FunJ5aSW-L^LEDh!dng^$L8B4 z$2)6v#5O+VViHX#XJCII5#w0Edc*wzgX|2xi136vjaD5yiX490DoQAH-C#X>MDB0e zy_(f?m)HKz6#sj0GQVHqgDWCze!gI;Op>pC>Cc*f&1t?ada0fYN{hR@yO&q(mAiC~ zxBog)V4izgV_O=+9%u-xhBhZo>+R|=FOYB#}&`&zNmJoaSC79>(d!>?)>@t1`?Of z98vyu=w6*$US~OTbJ*=O%HNtAIV$that;g4>Rj8{*YQWCtWxpQ1M7`Sa+5_RHZm#S zY<9P3lvMU~TsPtK&rE}~mLog*UQSsPaK**N&f6%d+}Z=RW6|X z&VFXOm|tINmz|$b-hV2=Kto@wThIQ&^0V?9&)qzDcD^>uE$|A{NK?7%(4xWrrQG73 zN73^`S3+fjzeq;Tlb-Zhw0EJsQ__q0{>>Y z#ebceq~YXa<#~?mP>I66!u_O4*eo=uflIj`Fo#jGC2?G7a9~~aWOAn2Bhrv bG~SO{`q5!wK;{`>4q)(f^>bP0l+XkKDWGIX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..530b9d3ddad366065e761265d2cb26520cc94c13 GIT binary patch literal 6429 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 r&g?~)4A1dWhZ!52XV - - + + + + \ 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 0000000000000000000000000000000000000000..5cdb6931fe5611336ebe63119d9338c249e6aeb8 GIT binary patch literal 58514 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 literal 0 HcmV?d00001 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")