预览图片、视频、删除消息、消息通知铃声

This commit is contained in:
itgaojian163 2024-11-05 17:49:37 +08:00
parent db9ad383fa
commit 06f10d3ed3
55 changed files with 2017 additions and 88 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

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

View File

@ -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">
<activity
android:name=".page.activity.LoginActivity"
android:exported="false" />
<activity
android:name=".page.activity.ChatActivity"
android:exported="false" />
<activity
android:name=".page.activity.SplashActivity"
android:exported="true"
android:theme="@style/Theme.Aimz_k">
android:theme="@style/Anim_fade">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".page.activity.LoginActivity"
android:exported="false" />
<activity
android:name=".page.activity.ChatActivity"
android:exported="false" />
<activity
android:name=".page.activity.MainActivity"
android:exported="false"

View File

@ -2,14 +2,15 @@ package com.tenlionsoft.aimz_k.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import com.lxj.xpopup.XPopup
import com.tenlionsoft.aimz_k.databinding.ItemCategoryBinding
import com.tenlionsoft.aimz_k.model.MsgCategoryBean
import com.tenlionsoft.aimz_k.viewmodel.MsgViewModel
import com.tenlionsoft.baselib.base.BaseBindingAdapter
class CategoryAdapter(
datas: List<MsgCategoryBean>,
private val viewModel: MsgViewModel
datas: List<MsgCategoryBean>, private val viewModel: MsgViewModel
) : BaseBindingAdapter<MsgCategoryBean, ItemCategoryBinding>(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
}
}
}

View File

@ -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)
/**
* 添加消息多个
*

View File

@ -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)
/**
* 添加消息多个

View File

@ -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<MsgBean> {
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<MsgBean> {
mBinding.rlvChats.postDelayed({
layoutManager.scrollToPositionWithOffset(
itemCount - 1,
0
itemCount - 1, 0
)
}, 50)
chatPageViewModel!!.scrollListToBottom.value = false
@ -182,8 +184,7 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener<MsgBean> {
bottomHeight = mBinding.llBottomLayout.height
}
}
SoftKeyBoardListener().setChangeListener(this@ChatActivity,
{ h ->
SoftKeyBoardListener().setChangeListener(this@ChatActivity, { h ->
Log.e("ChatActivity", "软键盘: 显示")
chatPageViewModel!!.showReplyLayout.value = false
chatPageViewModel!!.showEmojiLayout.value = false
@ -198,12 +199,10 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener<MsgBean> {
//滚动到底部
mBinding.rlvChats.postDelayed({
layoutManager.scrollToPositionWithOffset(
itemCount - 1,
0
itemCount - 1, 0
)
}, 100)
},
{ _ ->
}, { _ ->
Log.e("ChatActivity", "软键盘: 隐藏")
val isShowOther =
chatPageViewModel!!.showReplyLayout.value!! || chatPageViewModel!!.showEmojiLayout.value!! || chatPageViewModel!!.showChooseLayout.value!!
@ -242,8 +241,7 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener<MsgBean> {
//滚动到底部
mBinding.rlvChats.postDelayed({
layoutManager.scrollToPositionWithOffset(
itemCount - 1,
0
itemCount - 1, 0
)
}, 100)
}
@ -274,6 +272,34 @@ class ChatActivity : BaseActivity(), AdapterItemClickListener<MsgBean> {
//条目点击
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)
}
}
}
}

View File

@ -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<MsgCategoryBean>,
MainActivity.OnSocketConnectListener {
MainActivity.OnSocketConnectListener, AdapterItemLongClickListener<MsgCategoryBean> {
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<MsgCategoryBean>,
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 <T : ViewModel> create(modelClass: Class<T>): 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<MsgCategoryBean>,
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<MsgCategoryBean>,
/**
* 条目点击
*/
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<MsgCategoryBean>,
}
}
//长按
override fun onItemLongClick(type: Int, d: MsgCategoryBean) {
//删除聊天
viewModel.doDelChat(d)
}
}

View File

@ -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()
}
}
}

View File

@ -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<String>("")
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

View File

@ -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<List<MsgCategoryBean>>()
var adapter: CategoryAdapter = CategoryAdapter(_pwdList.value ?: emptyList(), this)
var onItemClickListener: AdapterItemClickListener<MsgCategoryBean>? = null
var onItemLongClickListener: AdapterItemLongClickListener<MsgCategoryBean>? = 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()
}
}
}

View File

@ -2,4 +2,16 @@
<resources>
<style name="Theme.Aimz_k" parent="Theme.AppCompat.Light.NoActionBar" />
<style name="Anim_fade" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowEnterAnimation">@anim/fade_in</item>
<item name="android:windowExitAnimation">@anim/fade_out</item>
</style>
<style name="fade" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/fade_in</item>
<item name="android:activityOpenExitAnimation">@anim/fade_out</item>
<item name="android:activityCloseEnterAnimation">@anim/fade_in</item>
<item name="android:activityCloseExitAnimation">@anim/fade_out</item>
</style>
</resources>

View File

@ -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)
}

View File

@ -0,0 +1,5 @@
package com.tenlionsoft.baselib.widget
interface AdapterItemLongClickListener<D> {
fun onItemLongClick(type: Int, d: D)
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="0.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:fromAlpha="1.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toAlpha="0.0" />

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%p"
android:toXDelta="0"
android:duration="500" />
</set>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0"
android:toXDelta="-100%p"
android:duration="500" />
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="160dp"
android:layout_height="100dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shp_tr_radius_5"
android:orientation="vertical">
<LinearLayout
android:layout_width="160dp"
android:layout_height="100dp"
android:layout_width="120dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/pb_state_loading"
android:layout_width="40dp"
android:layout_height="40dp"
android:indeterminateBehavior="repeat"
android:indeterminateDrawable="@drawable/anim_loading" />
<TextView
android:id="@+id/tipTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/pb_state_loading"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:text="处理中..."
android:textColor="@color/black" />
<ProgressBar
android:id="@+id/pb_state_loading"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_marginTop="8dp"
android:indeterminateBehavior="repeat"
android:indeterminateDrawable="@drawable/anim_loading" />
</LinearLayout>
</RelativeLayout>

Binary file not shown.

View File

@ -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" }

1
medialib/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

60
medialib/build.gradle.kts Normal file
View File

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

View File

21
medialib/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".base.SimpleVideoActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false" />
<activity
android:name=".base.SimplePhotoActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false" />
</application>
</manifest>

View File

@ -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<String>? = 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"
}
}

View File

@ -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()
}
}

View File

@ -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<String>) :
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<PhotoView>(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`
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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?)
}

View File

@ -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?)
}

View File

@ -0,0 +1,7 @@
package com.tenlionsoft.medialib.base.widget
import android.widget.ImageView
interface OnPhotoTapListener {
fun onPhotoTap(view: ImageView?, x: Float, y: Float)
}

View File

@ -0,0 +1,5 @@
package com.tenlionsoft.medialib.base.widget
interface OnScaleChangedListener {
fun onScaleChange(scaleFactor: Float, focusX: Float, focusY: Float)
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package com.tenlionsoft.medialib.base.widget
interface OnViewDragListener {
fun onDrag(dx: Float, dy: Float)
}

View File

@ -0,0 +1,7 @@
package com.tenlionsoft.medialib.base.widget
import android.view.View
interface OnViewTapListener {
fun onViewTap(view: View?, x: Float, y: Float)
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<RelativeLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_images"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tv_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="20dp"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="1 / 2" />
</RelativeLayout>
</layout>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".base.SimpleVideoActivity">
<com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
android:id="@+id/s_video"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.tenlionsoft.medialib.base.widget.PhotoView
android:id="@+id/pv_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="VideoView">
<attr name="looping" format="boolean"/>
<attr name="enableAudioFocus" format="boolean"/>
<attr name="screenScaleType" format="dimension">
<enum name="type_default" value="0"/>
<enum name="type_16_9" value="1"/>
<enum name="type_4_3" value="2"/>
<enum name="type_match_parent" value="3"/>
<enum name="type_original" value="4"/>
<enum name="type_center_crop" value="5"/>
</attr>
<attr name="playerBackgroundColor" format="color"/>
</declare-styleable>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="dkplayer_theme_color">#9F1512</color>
<color name="dkplayer_theme_color_translucent">#999F1512</color>
<color name="dkplayer_background_color">#4D000000</color>
</resources>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dkplayer_controller_height">46dp</dimen>
<dimen name="dkplayer_controller_icon_padding">12dp</dimen>
<dimen name="dkplayer_controller_text_size">16sp</dimen>
<dimen name="dkplayer_default_spacing">10dp</dimen>
<dimen name="dkplayer_controller_seekbar_size_n">14dp</dimen>
<dimen name="dkplayer_controller_seekbar_size_s">14dp</dimen>
<dimen name="dkplayer_controller_seekbar_max_size">1dp</dimen>
<dimen name="dkplayer_play_btn_size">50dp</dimen>
<dimen name="dkplayer_controller_time_text_size">14sp</dimen>
</resources>

View File

@ -0,0 +1,11 @@
<resources>
<string name="dkplayer_replay">重新播放</string>
<string name="dkplayer_lock_tip">请先解锁屏幕!</string>
<string name="dkplayer_unlocked">已解锁</string>
<string name="dkplayer_locked">已锁定</string>
<string name="dkplayer_error_message">出了点小问题,请稍后重试</string>
<string name="dkplayer_retry"> 重 试 </string>
<string name="dkplayer_continue_play">继续播放</string>
<string name="dkplayer_wifi_tip">您正在使用移动网络,继续播放将消耗流量</string>
<string name="img_position">%1$d&#160;&#160;/&#160;&#160;%2$d</string>
</resources>

View File

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

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