diff --git a/app/src/main/java/com/all/pdfreader/pro/app/model/FileActionEvent.kt b/app/src/main/java/com/all/pdfreader/pro/app/model/FileActionEvent.kt index 16f4cd0..d4769c0 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/model/FileActionEvent.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/model/FileActionEvent.kt @@ -1,5 +1,7 @@ package com.all.pdfreader.pro.app.model +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData import java.io.File sealed class FileActionEvent { @@ -16,4 +18,19 @@ sealed class FileActionEvent { data class RemovePassword(val status: Status, val success: Boolean? = null) : FileActionEvent() { enum class Status { START, COMPLETE } } + + //只做通知,不携带任何数据,使用object。 + object NoticeReload : FileActionEvent() + + inline fun LiveData.observeEvent( + owner: LifecycleOwner, + crossinline handler: (T) -> Unit + ) { + this.observe(owner) { event -> + if (event is T) { + handler(event) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt b/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt index 0aa87fd..55fb973 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/sp/AppStore.kt @@ -20,14 +20,33 @@ class AppStore(context: Context) { key = DOCUMENT_SORT_TYPE, defaultValue = SortConfig.default().toPreferenceString() ) + // 是否开启护眼遮罩 var isEyeCareMode: Boolean by store.boolean( key = IS_EYE_CARE_MODE, defaultValue = false ) + // 横or竖 + var isVertical: Boolean by store.boolean( + key = IS_VERTICAL, defaultValue = true//默认竖 + ) + + // 是否逐页 + var isPageFling: Boolean by store.boolean( + key = IS_PAGE_FLING, defaultValue = false + ) + + // 是否反转颜色 + var isColorInversion: Boolean by store.boolean( + key = IS_COLOR_INVERSION, defaultValue = false + ) + companion object { private const val FILE_NAME = "prp_sp_name" private const val PERMISSIONS_DIALOG_PROMPT = "permissions_dialog_prompt" private const val DOCUMENT_SORT_TYPE = "document_sort_type" private const val IS_EYE_CARE_MODE = "is_eye_care_mode" + private const val IS_VERTICAL = "is_vertical" + private const val IS_PAGE_FLING = "is_page_fling" + private const val IS_COLOR_INVERSION = "is_color_inversion" } } \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt index 9caf22f..bb761a2 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/MainActivity.kt @@ -22,6 +22,7 @@ import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation import com.all.pdfreader.pro.app.util.PdfScanner import com.all.pdfreader.pro.app.util.StoragePermissionHelper import com.all.pdfreader.pro.app.viewmodel.PdfViewModel +import com.all.pdfreader.pro.app.viewmodel.observeEvent import com.gyf.immersionbar.ImmersionBar import kotlinx.coroutines.launch @@ -71,75 +72,66 @@ class MainActivity : BaseActivity(), PermissionDialogFragment.PermissionCallback private fun initObserve() { //观察其余操作 - viewModel.fileActionEvent.observe(this) { event -> - when (event) { - is FileActionEvent.Rename -> { - if (event.renameResult.success) { - showToast(getString(R.string.rename_successfully)) + viewModel.fileActionEvent.observeEvent(this) { event -> + if (event.renameResult.success) { + showToast(getString(R.string.rename_successfully)) + } else { + showToast(event.renameResult.errorMessage.toString()) + } + } + viewModel.fileActionEvent.observeEvent(this){ event -> + if (event.deleteResult.success) { + showToast(getString(R.string.delete_successfully)) + } else { + showToast(event.deleteResult.errorMessage.toString()) + } + } + viewModel.fileActionEvent.observeEvent(this){ event -> + if (event.isFavorite) { + showToast(getString(R.string.added_to_favorites)) + } else { + showToast(getString(R.string.removed_from_favorites)) + } + } + viewModel.fileActionEvent.observeEvent(this){ event -> + if (event.file != null) { + showToast(getString(R.string.duplicate_created_successfully)) + } else { + showToast(getString(R.string.duplicate_created_failed)) + } + } + viewModel.fileActionEvent.observeEvent(this){ event -> + when (event.status) { + FileActionEvent.SetPassword.Status.START -> { + progressDialog = ProgressDialogFragment() + progressDialog?.show(supportFragmentManager, "progressDialog") + } + + FileActionEvent.SetPassword.Status.COMPLETE -> { + progressDialog?.dismiss() + progressDialog = null + if (event.success == true) { + showToast(getString(R.string.set_password_successfully)) } else { - showToast(event.renameResult.errorMessage.toString()) + showToast(getString(R.string.set_password_failed)) } } + } + } + viewModel.fileActionEvent.observeEvent(this){ event -> + when (event.status) { + FileActionEvent.RemovePassword.Status.START -> { + progressDialog = ProgressDialogFragment() + progressDialog?.show(supportFragmentManager, "progressDialog") + } - is FileActionEvent.Delete -> { - if (event.deleteResult.success) { - showToast(getString(R.string.delete_successfully)) + FileActionEvent.RemovePassword.Status.COMPLETE -> { + progressDialog?.dismiss() + progressDialog = null + if (event.success == true) { + showToast(getString(R.string.remove_password_successfully)) } else { - showToast(event.deleteResult.errorMessage.toString()) - } - } - - is FileActionEvent.Favorite -> { - if (event.isFavorite) { - showToast(getString(R.string.added_to_favorites)) - } else { - showToast(getString(R.string.removed_from_favorites)) - } - } - - is FileActionEvent.Duplicate -> { - if (event.file != null) { - showToast(getString(R.string.duplicate_created_successfully)) - } else { - showToast(getString(R.string.duplicate_created_failed)) - } - } - - is FileActionEvent.SetPassword -> { - when (event.status) { - FileActionEvent.SetPassword.Status.START -> { - progressDialog = ProgressDialogFragment() - progressDialog?.show(supportFragmentManager, "progressDialog") - } - - FileActionEvent.SetPassword.Status.COMPLETE -> { - progressDialog?.dismiss() - progressDialog = null - if (event.success == true) { - showToast(getString(R.string.set_password_successfully)) - } else { - showToast(getString(R.string.set_password_failed)) - } - } - } - } - - is FileActionEvent.RemovePassword -> { - when (event.status) { - FileActionEvent.RemovePassword.Status.START -> { - progressDialog = ProgressDialogFragment() - progressDialog?.show(supportFragmentManager, "progressDialog") - } - - FileActionEvent.RemovePassword.Status.COMPLETE -> { - progressDialog?.dismiss() - progressDialog = null - if (event.success == true) { - showToast(getString(R.string.remove_password_successfully)) - } else { - showToast(getString(R.string.remove_password_failed)) - } - } + showToast(getString(R.string.remove_password_failed)) } } } diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt index 9c649de..c96cc9a 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/act/PdfViewActivity.kt @@ -9,10 +9,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.all.pdfreader.pro.app.R import com.all.pdfreader.pro.app.databinding.ActivityPdfViewBinding +import com.all.pdfreader.pro.app.model.FileActionEvent import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.ui.dialog.PdfPasswordProtectionDialogFragment +import com.all.pdfreader.pro.app.ui.dialog.ViewModelDialogFragment import com.all.pdfreader.pro.app.ui.view.CustomScrollHandle import com.all.pdfreader.pro.app.viewmodel.PdfViewModel +import com.all.pdfreader.pro.app.viewmodel.observeEvent +import com.github.barteksc.pdfviewer.PDFView import com.github.barteksc.pdfviewer.listener.OnErrorListener import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener import com.github.barteksc.pdfviewer.listener.OnPageChangeListener @@ -69,6 +73,12 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList finish() } } + + viewModel.fileActionEvent.observeEvent(this) { + logDebug("observeEvent NoticeReload") + val file = File(pdfDocument.filePath) + loadPdfInternal(file, null) + } } private fun setupOnClick() { @@ -80,7 +90,9 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList toggleEyeCareMode(appStore.isEyeCareMode) } binding.viewModelBtn.setOnClickListener { - + ViewModelDialogFragment(pdfDocument.filePath).show( + supportFragmentManager, "ViewModelDialogFragment" + ) } } @@ -102,7 +114,8 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList //PDF 文档加载完成时回调 override fun loadComplete(nbPages: Int) { - + //加载完毕进行一次缩放 + binding.pdfview.resetZoomWithAnimation() } //PDF 加载出错时回调 @@ -161,20 +174,27 @@ class PdfViewActivity : BaseActivity(), OnLoadCompleteListener, OnPageChangeList } private fun loadPdfInternal(file: File, password: String?) { - binding.pdfview.fromFile(file) - .apply { - password?.let { password(it) } // 只有在有密码时才调用 - defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始 - enableDoubletap(true) // 是否允许双击缩放 - enableAnnotationRendering(true) // 是否渲染注释 - onLoad(this@PdfViewActivity) // 加载回调 - onError(this@PdfViewActivity) // 错误回调 - onTap(this@PdfViewActivity) // 单击回调 - onPageChange(this@PdfViewActivity) // 页面改变回调 - scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示 - pageFling(true)//逐页滑动 + binding.pdfview.fromFile(file).apply { + password?.let { password(it) } // 只有在有密码时才调用 + defaultPage(pdfDocument.lastReadPage) // 从上次阅读页码开始 + enableDoubletap(true) // 是否允许双击缩放 + enableAnnotationRendering(true) // 是否渲染注释 + onLoad(this@PdfViewActivity) // 加载回调 + onError(this@PdfViewActivity) // 错误回调 + onTap(this@PdfViewActivity) // 单击回调 + onPageChange(this@PdfViewActivity) // 页面改变回调 + scrollHandle(CustomScrollHandle(this@PdfViewActivity)) // 自定义的页数展示 + if (appStore.isPageFling) { + pageSnap(true)//页面可在逐页中,可居中展示,非逐页模式,滑动停止后可自动定格在居中位置。 + autoSpacing(true)//开启逐页就开启自动间距 + pageFling(true)//逐页 + } else { + spacing(10) + pageFling(false) } - .load() + swipeHorizontal(!appStore.isVertical) + nightMode(appStore.isColorInversion) + }.load() } private fun toggleFullScreen() { diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ViewModelDialogFragment.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ViewModelDialogFragment.kt index 7d3a1da..1d572d9 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ViewModelDialogFragment.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/dialog/ViewModelDialogFragment.kt @@ -1,36 +1,32 @@ package com.all.pdfreader.pro.app.ui.dialog -import android.net.Uri +import android.animation.ValueAnimator +import android.graphics.PorterDuff import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ImageView +import android.widget.TextView import android.widget.Toast import androidx.fragment.app.activityViewModels import com.all.pdfreader.pro.app.R -import com.all.pdfreader.pro.app.databinding.DialogListMoreBinding import com.all.pdfreader.pro.app.databinding.DialogViewModelBinding -import com.all.pdfreader.pro.app.model.PrintResult import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity -import com.all.pdfreader.pro.app.util.AppUtils.dpToPx -import com.all.pdfreader.pro.app.util.AppUtils.printPdfFile -import com.all.pdfreader.pro.app.util.AppUtils.setClickWithAnimation -import com.all.pdfreader.pro.app.util.AppUtils.shareFile -import com.all.pdfreader.pro.app.util.FileUtils.toFormatFileSize -import com.all.pdfreader.pro.app.util.FileUtils.toSlashDate +import com.all.pdfreader.pro.app.sp.AppStore +import com.all.pdfreader.pro.app.ui.view.CustomSwitchButton +import com.all.pdfreader.pro.app.ui.view.CustomSwitchButton.OnCheckedChangeListener import com.all.pdfreader.pro.app.viewmodel.PdfViewModel -import com.bumptech.glide.Glide -import com.bumptech.glide.load.resource.bitmap.CenterCrop -import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import java.io.File class ViewModelDialogFragment(val filePath: String) : BottomSheetDialogFragment() { private lateinit var binding: DialogViewModelBinding - private val viewModel: PdfViewModel by activityViewModels() + private val viewModel: PdfViewModel by activityViewModels()//为PdfViewActivity的PdfViewModel private lateinit var pdfDocument: PdfDocumentEntity - private var isFavorite: Boolean = false + private var isFirstSetSelected = true// 第一次直接设置位置,不做指示器的切换动画 + private val appstore by lazy { AppStore(requireActivity()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -59,127 +55,109 @@ class ViewModelDialogFragment(val filePath: String) : BottomSheetDialogFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.pdfDocument.observe(this) { document -> - document?.let { - pdfDocument = it - isFavorite = pdfDocument.isFavorite - initUi() - setupOnClick() - } ?: run { - showToast(getString(R.string.file_not)) - dismiss() - } + viewModel.pdfDocument.value?.let { + pdfDocument = it + initUi() + setupOnClick() + } ?: run { + showToast(getString(R.string.file_not)) + dismiss() } - viewModel.getPDFDocument(filePath) } private fun initUi() { - binding.tvFileName.text = pdfDocument.fileName - binding.tvFileSize.text = pdfDocument.fileSize.toFormatFileSize() - binding.tvFileDate.text = pdfDocument.lastModified.toSlashDate() - if (pdfDocument.isPassword) { - binding.lockLayout.visibility = View.VISIBLE - binding.tvFileImg.visibility = View.GONE - } else { - binding.lockLayout.visibility = View.GONE - binding.tvFileImg.visibility = View.VISIBLE - Glide.with(binding.root).load(pdfDocument.thumbnailPath) - .transform(CenterCrop(), RoundedCorners(8.dpToPx(binding.root.context))) - .into(binding.tvFileImg) + // 初始化指示器宽度 + binding.indicator.post { + val indicatorWidth = binding.btnHorizontal.width + binding.indicator.layoutParams.width = indicatorWidth + binding.indicator.requestLayout() + setSelectedMode(appstore.isVertical) } - updateCollectUi(isFavorite) - updatePasswordUi(pdfDocument.isPassword) + binding.switchPageByPage.setChecked(appstore.isPageFling) + binding.switchColorInversion.setChecked(appstore.isColorInversion) } private fun setupOnClick() { - binding.collectBtn.setClickWithAnimation(duration = 250) { - isFavorite = !isFavorite - updateCollectUi(isFavorite) - viewModel.saveCollectState(pdfDocument.filePath, isFavorite) - dismiss() + binding.btnHorizontal.setOnClickListener { + setSelectedMode(isVertical = false) + AppStore(requireActivity()).isVertical = false + viewModel.noticeReloadPdf() } - binding.renameFileBtn.setOnClickListener { - RenameDialogFragment().show(parentFragmentManager, "ListMoreDialogFragment") - dismiss() + binding.btnVertical.setOnClickListener { + setSelectedMode(isVertical = true) + AppStore(requireActivity()).isVertical = true + viewModel.noticeReloadPdf() } - binding.deleteFileBtn.setOnClickListener { - DeleteDialogFragment().show(parentFragmentManager, "DeleteDialogFragment") - dismiss() - } - binding.detailsBtn.setOnClickListener { - FileDetailsDialogFragment().show(parentFragmentManager, "FileDetailsDialogFragment") - dismiss() - } - binding.shareBtn.setOnClickListener { - shareFile(requireActivity(), File(pdfDocument.filePath)) - dismiss() - } - binding.printBtn.setOnClickListener { - val result = printPdfFile(requireActivity(), Uri.fromFile(File(pdfDocument.filePath))) - when (result) { - PrintResult.DeviceNotSupported -> { - Toast.makeText( - context, - R.string.device_does_not_support_printing, - Toast.LENGTH_LONG - ).show() - } - - is PrintResult.Error -> { - Toast.makeText(context, R.string.pdf_cannot_print_error, Toast.LENGTH_LONG) - .show() - } - - PrintResult.MalformedPdf -> { - Toast.makeText(context, R.string.cannot_print_malformed_pdf, Toast.LENGTH_LONG) - .show() - } - - PrintResult.PasswordRequired -> { - Toast.makeText( - context, - R.string.pdf_cant_print_password_protected, - Toast.LENGTH_LONG - ).show() - } - - PrintResult.Success -> { - - } + binding.switchPageByPage.setOnCheckedChangeListener(object : OnCheckedChangeListener { + override fun onCheckedChanged(view: CustomSwitchButton?, isChecked: Boolean) { + view?.setChecked(isChecked) + appstore.isPageFling = isChecked + viewModel.noticeReloadPdf() } - dismiss() - } - binding.duplicateFileBtn.setOnClickListener { - viewModel.duplicateFile(requireActivity(), pdfDocument.filePath) - dismiss() - } - binding.setPasswordBtn.setOnClickListener { - if (pdfDocument.isPassword) { - PdfRemovePasswordDialog().show(parentFragmentManager, "PdfRemovePasswordDialog") + }) + binding.switchColorInversion.setOnCheckedChangeListener(object : OnCheckedChangeListener { + override fun onCheckedChanged( + view: CustomSwitchButton?, + isChecked: Boolean + ) { + view?.setChecked(isChecked) + appstore.isColorInversion = isChecked + viewModel.noticeReloadPdf() + } + }) + } + + private fun setSelectedMode(isVertical: Boolean) { + binding.apply { + val targetX = if (isVertical) btnVertical.x else btnHorizontal.x + + if (isFirstSetSelected) { + indicator.translationX = targetX + isFirstSetSelected = false } else { - PdfSetPasswordDialog().show(parentFragmentManager, "PdfSetPasswordDialog") + indicator.animate().translationX(targetX).setDuration(250) + .setInterpolator(AccelerateDecelerateInterpolator()).start() } - dismiss() + + // 横向 + animateIconAndTextColor( + horizontalTv, + horizontalIv, + if (isVertical) requireContext().getColor(R.color.white) + else requireContext().getColor(R.color.icon_color), + if (isVertical) requireContext().getColor(R.color.icon_color) + else requireContext().getColor(R.color.white) + ) + // 垂直 + animateIconAndTextColor( + verticalTv, + verticalIv, + if (isVertical) requireContext().getColor(R.color.icon_color) + else requireContext().getColor(R.color.white), + if (isVertical) requireContext().getColor(R.color.white) + else requireContext().getColor(R.color.icon_color) + ) } } - private fun updateCollectUi(b: Boolean) { - if (b) { - binding.collectIv.setImageResource(R.drawable.collected) - } else { - binding.collectIv.setImageResource(R.drawable.collect) + + /** + * 改变img与text的颜色值,动画渐变同步indicator的时间 + */ + private fun animateIconAndTextColor( + textView: TextView, imageView: ImageView, fromColor: Int, toColor: Int + ) { + ValueAnimator.ofArgb(fromColor, toColor).apply { + duration = 250 + addUpdateListener { animator -> + val animatedColor = animator.animatedValue as Int + textView.setTextColor(animatedColor) + imageView.setColorFilter(animatedColor, PorterDuff.Mode.SRC_IN) + } + start() } } - private fun updatePasswordUi(b: Boolean) { - if (b) { - binding.passwordIv.setImageResource(R.drawable.unlock) - binding.passwordTv.text = getString(R.string.remove_password) - } else { - binding.passwordIv.setImageResource(R.drawable.lock) - binding.passwordTv.text = getString(R.string.set_password) - } - } private fun showToast(message: String) { Toast.makeText(requireActivity(), message, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomSwitchButton.kt b/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomSwitchButton.kt new file mode 100644 index 0000000..e740bf1 --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/ui/view/CustomSwitchButton.kt @@ -0,0 +1,1158 @@ +package com.all.pdfreader.pro.app.ui.view + +import android.animation.Animator +import android.animation.Animator.AnimatorListener +import android.animation.ArgbEvaluator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener +import android.content.Context +import android.content.res.Resources +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.os.Build +import android.util.AttributeSet +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.widget.Checkable +import com.all.pdfreader.pro.app.R +import kotlin.math.max +import kotlin.math.min + +/** + * CustomSwitchButton. + */ +class CustomSwitchButton : View, Checkable { + /** + * 动画状态: + * 1.静止 + * 2.进入拖动 + * 3.处于拖动 + * 4.拖动-复位 + * 5.拖动-切换 + * 6.点击切换 + */ + private val ANIMATE_STATE_NONE = 0 + private val ANIMATE_STATE_PENDING_DRAG = 1 + private val ANIMATE_STATE_DRAGING = 2 + private val ANIMATE_STATE_PENDING_RESET = 3 + private val ANIMATE_STATE_PENDING_SETTLE = 4 + private val ANIMATE_STATE_SWITCH = 5 + + constructor(context: Context) : super(context) { + init(context, null) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context, attrs) + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) { + init(context, attrs) + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(0, 0, 0, 0) + } + + /** + * 初始化参数 + */ + private fun init(context: Context, attrs: AttributeSet?) { + var typedArray: TypedArray? = null + if (attrs != null) { + typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwitchButton) + } + shadowEffect = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_shadow_effect, + true + ) + uncheckCircleColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_color, + -0x555556 + ) //0XffAAAAAA; + uncheckCircleWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_width, + dp2pxInt(1.5f) + ) //dp2pxInt(1.5f); + uncheckCircleOffsetX = dp2px(10f) + uncheckCircleRadius = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_uncheckcircle_radius, + dp2px(4f) + ) //dp2px(4); + checkedLineOffsetX = dp2px(4f) + checkedLineOffsetY = dp2px(4f) + shadowRadius = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_shadow_radius, + dp2pxInt(2.5f) + ) //dp2pxInt(2.5f); + shadowOffset = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_shadow_offset, + dp2pxInt(1.5f) + ) //dp2pxInt(1.5f); + shadowColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_shadow_color, + 0X33000000 + ) //0X33000000; + uncheckColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheck_color, + -0x222223 + ) //0XffDDDDDD; + checkedColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checked_color, + -0xae2c99 + ) //0Xff51d367; + borderWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_border_width, + dp2pxInt(1f) + ) //dp2pxInt(1); + checkLineColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checkline_color, + Color.WHITE + ) //Color.WHITE; + checkLineWidth = optPixelSize( + typedArray, + R.styleable.SwitchButton_sb_checkline_width, + dp2pxInt(1f) + ) //dp2pxInt(1.0f); + checkLineLength = dp2px(6f) + val buttonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_button_color, + Color.WHITE + ) //Color.WHITE; + uncheckButtonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_uncheckbutton_color, + buttonColor + ) + checkedButtonColor = optColor( + typedArray, + R.styleable.SwitchButton_sb_checkedbutton_color, + buttonColor + ) + val effectDuration = optInt( + typedArray, + R.styleable.SwitchButton_sb_effect_duration, + 300 + ) //300; + isChecked = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_checked, + false + ) + showIndicator = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_show_indicator, + true + ) + background = optColor( + typedArray, + R.styleable.SwitchButton_sb_background, + Color.WHITE + ) //Color.WHITE; + enableEffect = optBoolean( + typedArray, + R.styleable.SwitchButton_sb_enable_effect, + true + ) + typedArray?.recycle() + paint = Paint(Paint.ANTI_ALIAS_FLAG) + buttonPaint = Paint(Paint.ANTI_ALIAS_FLAG) + buttonPaint!!.color = buttonColor + if (shadowEffect) { + buttonPaint!!.setShadowLayer( + shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), + shadowColor + ) + } + viewState = ViewState() + beforeState = ViewState() + afterState = ViewState() + valueAnimator = ValueAnimator.ofFloat(0f, 1f) + valueAnimator?.duration = effectDuration.toLong() + valueAnimator?.repeatCount = 0 + valueAnimator?.addUpdateListener(animatorUpdateListener) + valueAnimator?.addListener(animatorListener) + super.setClickable(true) + setPadding(0, 0, 0, 0) + setLayerType(LAYER_TYPE_SOFTWARE, null) + + } + + override fun onMeasure(widthMeasureSpecParam: Int, heightMeasureSpecParam: Int) { + var widthMeasureSpec = widthMeasureSpecParam + var heightMeasureSpec = heightMeasureSpecParam + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + if (widthMode == MeasureSpec.UNSPECIFIED + || widthMode == MeasureSpec.AT_MOST + ) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_WIDTH, MeasureSpec.EXACTLY) + } + if (heightMode == MeasureSpec.UNSPECIFIED + || heightMode == MeasureSpec.AT_MOST + ) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_HEIGHT, MeasureSpec.EXACTLY) + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + val viewPadding = max(shadowRadius + shadowOffset, borderWidth).toFloat() + height = h - viewPadding - viewPadding + width = w - viewPadding - viewPadding + viewRadius = height * .5f + buttonRadius = viewRadius - borderWidth + left = viewPadding + top = viewPadding + right = w - viewPadding + bottom = h - viewPadding + centerX = (left + right) * .5f + centerY = (top + bottom) * .5f + buttonMinX = left + viewRadius + buttonMaxX = right - viewRadius + if (isChecked()) { + setCheckedViewState(viewState) + } else { + setUncheckViewState(viewState) + } + isUiInited = true + postInvalidate() + } + + /** + * @param viewState + */ + private fun setUncheckViewState(viewState: ViewState?) { + viewState?.radius = 0f + viewState?.checkStateColor = uncheckColor + viewState?.checkedLineColor = Color.TRANSPARENT + viewState?.buttonX = buttonMinX + buttonPaint?.color = uncheckButtonColor + } + + /** + * @param viewState + */ + private fun setCheckedViewState(viewState: ViewState?) { + viewState?.radius = viewRadius + viewState?.checkStateColor = checkedColor + viewState?.checkedLineColor = checkLineColor + viewState?.buttonX = buttonMaxX + buttonPaint?.color = checkedButtonColor + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + paint?.strokeWidth = borderWidth.toFloat() + paint?.style = Paint.Style.FILL + //绘制白色背景 + paint?.color = background + drawRoundRect( + canvas, + left, top, right, bottom, + viewRadius, paint + ) + //绘制关闭状态的边框 + paint?.style = Paint.Style.STROKE + paint?.color = uncheckColor + drawRoundRect( + canvas, + left, top, right, bottom, + viewRadius, paint + ) + + //绘制小圆圈 + if (showIndicator) { + drawUncheckIndicator(canvas) + } + + //绘制开启背景色 + val des = viewState!!.radius * .5f //[0-backgroundRadius*0.5f] + paint?.style = Paint.Style.STROKE + paint?.color = viewState?.checkStateColor ?: Color.TRANSPARENT + paint?.strokeWidth = borderWidth + des * 2f + drawRoundRect(canvas, left + des, top + des, right - des, bottom - des, viewRadius, paint) + + //绘制按钮左边绿色长条遮挡 + paint?.style = Paint.Style.FILL + paint?.strokeWidth = 1f + drawArc(canvas, left, top, left + 2 * viewRadius, top + 2 * viewRadius, 90f, 180f, paint) + paint?.let { canvas.drawRect(left + viewRadius, top, viewState?.buttonX ?: 0f, top + 2 * viewRadius, it) } + + //绘制小线条 + if (showIndicator) { + drawCheckedIndicator(canvas) + } + + //绘制按钮 + viewState?.buttonX?.let { drawButton(canvas, it, centerY) } + } + /** + * 绘制选中状态指示器 + * @param canvas + * @param color + * @param lineWidth + * @param sx + * @param sy + * @param ex + * @param ey + * @param paint + */ + /** + * 绘制选中状态指示器 + * @param canvas + */ + protected fun drawCheckedIndicator( + canvas: Canvas, + color: Int = viewState?.checkedLineColor ?: Color.TRANSPARENT, + lineWidth: Float = checkLineWidth.toFloat(), + sx: Float = left + viewRadius - checkedLineOffsetX, + sy: Float = centerY - checkLineLength, + ex: Float = left + viewRadius - checkedLineOffsetY, + ey: Float = centerY + checkLineLength, + paint: Paint? = this.paint + ) { + paint?.style = Paint.Style.STROKE + paint?.color = color + paint?.strokeWidth = lineWidth + paint?.let { canvas.drawLine(sx, sy, ex, ey, it) } + } + + /** + * 绘制关闭状态指示器 + * @param canvas + */ + private fun drawUncheckIndicator(canvas: Canvas) { + drawUncheckIndicator( + canvas, + uncheckCircleColor, + uncheckCircleWidth.toFloat(), + right - uncheckCircleOffsetX, centerY, + uncheckCircleRadius, + paint + ) + } + + /** + * 绘制关闭状态指示器 + * @param canvas + * @param color + * @param lineWidth + * @param centerX + * @param centerY + * @param radius + * @param paint + */ + protected fun drawUncheckIndicator( + canvas: Canvas, + color: Int, + lineWidth: Float, + centerX: Float, centerY: Float, + radius: Float, + paint: Paint? + ) { + paint?.style = Paint.Style.STROKE + paint?.color = color + paint?.strokeWidth = lineWidth + if (paint != null) { + canvas.drawCircle(centerX, centerY, radius, paint) + } + } + + /** + * @param canvas + * @param left + * @param top + * @param right + * @param bottom + * @param startAngle + * @param sweepAngle + * @param paint + */ + private fun drawArc( + canvas: Canvas, + left: Float, top: Float, + right: Float, bottom: Float, + startAngle: Float, + sweepAngle: Float, + paint: Paint? + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + canvas.drawArc( + left, top, right, bottom, + startAngle, sweepAngle, true, paint!! + ) + } else { + rect[left, top, right] = bottom + canvas.drawArc( + rect, + startAngle, sweepAngle, true, paint!! + ) + } + } + + /** + * @param canvas + * @param left + * @param top + * @param right + * @param bottom + * @param backgroundRadius + * @param paint + */ + private fun drawRoundRect( + canvas: Canvas, + left: Float, top: Float, + right: Float, bottom: Float, + backgroundRadius: Float, + paint: Paint? + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + canvas.drawRoundRect( + left, top, right, bottom, + backgroundRadius, backgroundRadius, paint!! + ) + } else { + rect[left, top, right] = bottom + canvas.drawRoundRect( + rect, + backgroundRadius, backgroundRadius, paint!! + ) + } + } + + /** + * @param canvas + * @param x px + * @param y px + */ + private fun drawButton(canvas: Canvas, x: Float, y: Float) { + buttonPaint?.let { canvas.drawCircle(x, y, buttonRadius, it) } + paint?.style = Paint.Style.STROKE + paint?.strokeWidth = 1f + paint?.color = -0x222223 + paint?.let { canvas.drawCircle(x, y, buttonRadius, it) } + } + + override fun setChecked(checked: Boolean) { + if (checked == isChecked()) { + postInvalidate() + return + } + toggle(enableEffect, false) + } + + override fun isChecked(): Boolean { + return isChecked + } + + override fun toggle() { + toggle(true) + } + + /** + * 切换状态 + * @param animate + */ + fun toggle(animate: Boolean) { + toggle(animate, true) + } + + private fun toggle(animate: Boolean, broadcast: Boolean) { + if (!isEnabled) { + return + } + if (isEventBroadcast) { + throw RuntimeException("should NOT switch the state in method: [onCheckedChanged]!") + } + if (!isUiInited) { + isChecked = !isChecked + if (broadcast) { + broadcastEvent() + } + return + } + if (valueAnimator!!.isRunning) { + valueAnimator!!.cancel() + } + if (!enableEffect || !animate) { + isChecked = !isChecked + if (isChecked()) { + setCheckedViewState(viewState) + } else { + setUncheckViewState(viewState) + } + postInvalidate() + if (broadcast) { + broadcastEvent() + } + return + } + animateState = ANIMATE_STATE_SWITCH + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + //切换到unchecked + setUncheckViewState(afterState) + } else { + setCheckedViewState(afterState) + } + valueAnimator!!.start() + } + + /** + * + */ + private fun broadcastEvent() { + if (onCheckedChangeListener != null) { + isEventBroadcast = true + onCheckedChangeListener?.onCheckedChanged(this, isChecked()) + } + isEventBroadcast = false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!isEnabled) { + return false + } + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + isTouchingDown = true + touchDownTime = System.currentTimeMillis() + //取消准备进入拖动状态 + removeCallbacks(postPendingDrag) + //预设100ms进入拖动状态 + postDelayed(postPendingDrag, 100) + } + MotionEvent.ACTION_MOVE -> { + val eventX = event.x + if (isPendingDragState) { + //在准备进入拖动状态过程中,可以拖动按钮位置 + var fraction = eventX / getWidth() + fraction = max(0f, min(1f, fraction)) + viewState?.buttonX = (buttonMinX + + (buttonMaxX - buttonMinX) + * fraction) + } else if (isDragState) { + //拖动按钮位置,同时改变对应的背景颜色 + var fraction = eventX / getWidth() + fraction = max(0f, min(1f, fraction)) + viewState?.buttonX = (buttonMinX + + (buttonMaxX - buttonMinX) + * fraction) + viewState?.checkStateColor = argbEvaluator.evaluate( + fraction, + uncheckColor, + checkedColor + ) as Int + postInvalidate() + } + } + MotionEvent.ACTION_UP -> { + isTouchingDown = false + //取消准备进入拖动状态 + removeCallbacks(postPendingDrag) + if (System.currentTimeMillis() - touchDownTime <= 300) { + //点击时间小于300ms,认为是点击操作 + toggle() + } else if (isDragState) { + //在拖动状态,计算按钮位置,设置是否切换状态 + val eventX = event.x + var fraction = eventX / getWidth() + fraction = Math.max(0f, Math.min(1f, fraction)) + val newCheck = fraction > .5f + if (newCheck == isChecked()) { + pendingCancelDragState() + } else { + isChecked = newCheck + pendingSettleState() + } + } else if (isPendingDragState) { + //在准备进入拖动状态过程中,取消之,复位 + pendingCancelDragState() + } + } + MotionEvent.ACTION_CANCEL -> { + isTouchingDown = false + removeCallbacks(postPendingDrag) + if (isPendingDragState + || isDragState + ) { + //复位 + pendingCancelDragState() + } + } + } + return true + } + + /** + * 是否在动画状态 + * @return + */ + private val isInAnimating: Boolean + get() = animateState != ANIMATE_STATE_NONE + + /** + * 是否在进入拖动或离开拖动状态 + * @return + */ + private val isPendingDragState: Boolean + get() = (animateState == ANIMATE_STATE_PENDING_DRAG + || animateState == ANIMATE_STATE_PENDING_RESET) + + /** + * 是否在手指拖动状态 + * @return + */ + private val isDragState: Boolean + get() = animateState == ANIMATE_STATE_DRAGING + + /** + * 设置是否启用阴影效果 + * @param shadowEffect true.启用 + */ + fun setShadowEffect(shadowEffect: Boolean) { + if (this.shadowEffect == shadowEffect) { + return + } + this.shadowEffect = shadowEffect + if (this.shadowEffect) { + buttonPaint!!.setShadowLayer( + shadowRadius.toFloat(), 0f, shadowOffset.toFloat(), + shadowColor + ) + } else { + buttonPaint!!.setShadowLayer( + 0f, 0f, 0f, + 0 + ) + } + } + + fun setEnableEffect(enable: Boolean) { + enableEffect = enable + } + + /** + * 开始进入拖动状态 + */ + private fun pendingDragState() { + if (isInAnimating) { + return + } + if (!isTouchingDown) { + return + } + if (valueAnimator!!.isRunning) { + valueAnimator!!.cancel() + } + animateState = ANIMATE_STATE_PENDING_DRAG + viewState?.let { beforeState?.copy(it) } + viewState?.let { afterState?.copy(it) } + if (isChecked()) { + afterState?.checkStateColor = checkedColor + afterState?.buttonX = buttonMaxX + afterState?.checkedLineColor = checkedColor + } else { + afterState?.checkStateColor = uncheckColor + afterState?.buttonX = buttonMinX + afterState?.radius = viewRadius + } + valueAnimator?.start() + } + + /** + * 取消拖动状态 + */ + private fun pendingCancelDragState() { + if (isDragState || isPendingDragState) { + if (valueAnimator?.isRunning == true) { + valueAnimator?.cancel() + } + animateState = ANIMATE_STATE_PENDING_RESET + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + setCheckedViewState(afterState) + } else { + setUncheckViewState(afterState) + } + valueAnimator?.start() + } + } + + /** + * 动画-设置新的状态 + */ + private fun pendingSettleState() { + if (valueAnimator?.isRunning == true) { + valueAnimator?.cancel() + } + animateState = ANIMATE_STATE_PENDING_SETTLE + viewState?.let { beforeState?.copy(it) } + if (isChecked()) { + setCheckedViewState(afterState) + } else { + setUncheckViewState(afterState) + } + valueAnimator?.start() + } + + override fun setOnClickListener(l: OnClickListener?) {} + override fun setOnLongClickListener(l: OnLongClickListener?) {} + fun setOnCheckedChangeListener(l: OnCheckedChangeListener?) { + onCheckedChangeListener = l + } + + interface OnCheckedChangeListener { + fun onCheckedChanged(view: CustomSwitchButton?, isChecked: Boolean) + } + /** */ + /** + * 阴影半径 + */ + private var shadowRadius = 0 + + /** + * 阴影Y偏移px + */ + private var shadowOffset = 0 + + /** + * 阴影颜色 + */ + private var shadowColor = 0 + + /** + * 背景半径 + */ + private var viewRadius = 0f + + /** + * 按钮半径 + */ + private var buttonRadius = 0f + + /** + * 背景高 + */ + private var height = 0f + + /** + * 背景宽 + */ + private var width = 0f + + /** + * 背景位置 + */ + private var left = 0f + private var top = 0f + private var right = 0f + private var bottom = 0f + private var centerX = 0f + private var centerY = 0f + + /** + * 背景底色 + */ + private var background = 0 + + /** + * 背景关闭颜色 + */ + private var uncheckColor = 0 + + /** + * 背景打开颜色 + */ + private var checkedColor = 0 + + /** + * 边框宽度px + */ + private var borderWidth = 0 + + /** + * 打开指示线颜色 + */ + private var checkLineColor = 0 + + /** + * 打开指示线宽 + */ + private var checkLineWidth = 0 + + /** + * 打开指示线长 + */ + private var checkLineLength = 0f + + /** + * 关闭圆圈颜色 + */ + private var uncheckCircleColor = 0 + + /** + * 关闭圆圈线宽 + */ + private var uncheckCircleWidth = 0 + + /** + * 关闭圆圈位移X + */ + private var uncheckCircleOffsetX = 0f + + /** + * 关闭圆圈半径 + */ + private var uncheckCircleRadius = 0f + + /** + * 打开指示线位移X + */ + private var checkedLineOffsetX = 0f + + /** + * 打开指示线位移Y + */ + private var checkedLineOffsetY = 0f + + /** + * Color for button when it's uncheck + */ + private var uncheckButtonColor = 0 + + /** + * Color for button when it's check + */ + private var checkedButtonColor = 0 + + /** + * 按钮最左边 + */ + private var buttonMinX = 0f + + /** + * 按钮最右边 + */ + private var buttonMaxX = 0f + + /** + * 按钮画笔 + */ + private var buttonPaint: Paint? = null + + /** + * 背景画笔 + */ + private var paint: Paint? = null + + /** + * 当前状态 + */ + private var viewState: ViewState? = null + private var beforeState: ViewState? = null + private var afterState: ViewState? = null + private val rect = RectF() + + /** + * 动画状态 + */ + private var animateState = ANIMATE_STATE_NONE + + /** + * + */ + private var valueAnimator: ValueAnimator? = null + private val argbEvaluator = ArgbEvaluator() + + /** + * 是否选中 + */ + private var isChecked = false + + /** + * 是否启用动画 + */ + private var enableEffect = false + + /** + * 是否启用阴影效果 + */ + private var shadowEffect = false + + /** + * 是否显示指示器 + */ + private var showIndicator = false + + /** + * 收拾是否按下 + */ + private var isTouchingDown = false + + /** + * + */ + private var isUiInited = false + + /** + * + */ + private var isEventBroadcast = false + private var onCheckedChangeListener: OnCheckedChangeListener? = null + + /** + * 手势按下的时刻 + */ + private var touchDownTime: Long = 0 + private val postPendingDrag = Runnable { + if (!isInAnimating) { + pendingDragState() + } + } + private val animatorUpdateListener: AnimatorUpdateListener = object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + val value = animation.animatedValue as Float + when (animateState) { + ANIMATE_STATE_PENDING_SETTLE -> { + run {} + run {} + run { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + } + ANIMATE_STATE_PENDING_RESET -> { + run {} + run { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + } + ANIMATE_STATE_PENDING_DRAG -> { + viewState!!.checkedLineColor = argbEvaluator.evaluate( + value, + beforeState!!.checkedLineColor, + afterState!!.checkedLineColor + ) as Int + viewState!!.radius = (beforeState!!.radius + + (afterState!!.radius - beforeState!!.radius) * value) + if (animateState != ANIMATE_STATE_PENDING_DRAG) { + viewState!!.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + } + viewState!!.checkStateColor = argbEvaluator.evaluate( + value, + beforeState!!.checkStateColor, + afterState!!.checkStateColor + ) as Int + } + ANIMATE_STATE_SWITCH -> { + viewState?.buttonX = (beforeState!!.buttonX + + (afterState!!.buttonX - beforeState!!.buttonX) * value) + val fraction = (viewState!!.buttonX - buttonMinX) / (buttonMaxX - buttonMinX) + viewState?.checkStateColor = argbEvaluator.evaluate( + fraction, + uncheckColor, + checkedColor + ) as Int + viewState?.radius = fraction * viewRadius + viewState?.checkedLineColor = argbEvaluator.evaluate( + fraction, + Color.TRANSPARENT, + checkLineColor + ) as Int + } + ANIMATE_STATE_DRAGING -> { + run {} + run {} + } + ANIMATE_STATE_NONE -> {} + else -> { + run {} + run {} + } + } + postInvalidate() + } + } + private val animatorListener: AnimatorListener = object : AnimatorListener { + override fun onAnimationStart(animation: Animator) {} + override fun onAnimationEnd(animation: Animator) { + when (animateState) { + ANIMATE_STATE_DRAGING -> {} + ANIMATE_STATE_PENDING_DRAG -> { + animateState = ANIMATE_STATE_DRAGING + viewState?.checkedLineColor = Color.TRANSPARENT + viewState?.radius = viewRadius + postInvalidate() + } + ANIMATE_STATE_PENDING_RESET -> { + animateState = ANIMATE_STATE_NONE + postInvalidate() + } + ANIMATE_STATE_PENDING_SETTLE -> { + animateState = ANIMATE_STATE_NONE + postInvalidate() + broadcastEvent() + } + ANIMATE_STATE_SWITCH -> { + isChecked = !isChecked + animateState = ANIMATE_STATE_NONE + postInvalidate() + broadcastEvent() + } + ANIMATE_STATE_NONE -> {} + else -> {} + } + } + + override fun onAnimationCancel(animation: Animator) {} + override fun onAnimationRepeat(animation: Animator) {} + } + /** */ + /** + * 保存动画状态 + */ + private class ViewState internal constructor() { + /** + * 按钮x位置[buttonMinX-buttonMaxX] + */ + var buttonX = 0f + + /** + * 状态背景颜色 + */ + var checkStateColor = 0 + + /** + * 选中线的颜色 + */ + var checkedLineColor = 0 + + /** + * 状态背景的半径 + */ + var radius = 0f + fun copy(source: ViewState) { + buttonX = source.buttonX + checkStateColor = source.checkStateColor + checkedLineColor = source.checkedLineColor + radius = source.radius + } + } + + companion object { + private val DEFAULT_WIDTH = dp2pxInt(58f) + private val DEFAULT_HEIGHT = dp2pxInt(36f) + + /** */ + private fun dp2px(dp: Float): Float { + val r = Resources.getSystem() + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.displayMetrics) + } + + private fun dp2pxInt(dp: Float): Int { + return dp2px(dp).toInt() + } + + private fun optInt( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getInt(index, def) ?: def + } + + private fun optPixelSize( + typedArray: TypedArray?, + index: Int, + def: Float + ): Float { + return typedArray?.getDimension(index, def) ?: def + } + + private fun optPixelSize( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getDimensionPixelOffset(index, def) ?: def + } + + private fun optColor( + typedArray: TypedArray?, + index: Int, + def: Int + ): Int { + return typedArray?.getColor(index, def) ?: def + } + + private fun optBoolean( + typedArray: TypedArray?, + index: Int, + def: Boolean + ): Boolean { + return typedArray?.getBoolean(index, def) ?: def + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt index e869966..5996ca0 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/util/PdfScanner.kt @@ -1,25 +1,18 @@ package com.all.pdfreader.pro.app.util import android.content.Context -import android.graphics.Bitmap -import android.graphics.Color -import android.os.ParcelFileDescriptor import android.util.Log -import androidx.core.graphics.createBitmap import com.all.pdfreader.pro.app.PRApp import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.repository.PdfRepository import com.all.pdfreader.pro.app.util.AppUtils.generateFastThumbnail import com.all.pdfreader.pro.app.util.FileUtils.isPdfEncrypted -import com.shockwave.pdfium.PdfDocument -import com.shockwave.pdfium.PdfiumCore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File -import java.io.FileOutputStream import java.util.concurrent.TimeUnit class PdfScanner( diff --git a/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/LiveDataExtensions.kt b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/LiveDataExtensions.kt new file mode 100644 index 0000000..b47d95f --- /dev/null +++ b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/LiveDataExtensions.kt @@ -0,0 +1,19 @@ +package com.all.pdfreader.pro.app.viewmodel + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import com.all.pdfreader.pro.app.model.FileActionEvent + +/** + * 公用观察FileActionEvent事件函数,只观察需要观察的响应 + */ +inline fun LiveData.observeEvent( + owner: LifecycleOwner, + crossinline handler: (T) -> Unit +) { + this.observe(owner) { event -> + if (event is T) { + handler(event) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt index d5e6712..a063c9b 100644 --- a/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt +++ b/app/src/main/java/com/all/pdfreader/pro/app/viewmodel/PdfViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.all.pdfreader.pro.app.PRApp import com.all.pdfreader.pro.app.model.FileActionEvent import com.all.pdfreader.pro.app.room.entity.PdfDocumentEntity import com.all.pdfreader.pro.app.room.repository.PdfRepository @@ -156,19 +157,40 @@ class PdfViewModel : ViewModel() { viewModelScope.launch { _fileActionEvent.postValue(FileActionEvent.RemovePassword(FileActionEvent.RemovePassword.Status.START)) - val success = withContext(Dispatchers.IO) { - PdfSecurityUtils.removePasswordFromPdf(filePath, password).also { - if (it) { + val success = try { + withContext(Dispatchers.IO) { + val removed = PdfSecurityUtils.removePasswordFromPdf(filePath, password) + Log.d("ocean", "密码移除$removed") + if (removed) { pdfRepository.updateIsPassword(filePath, false) + val document = pdfRepository.getDocumentByPath(filePath) + if (document?.thumbnailPath.isNullOrEmpty()) { + val newThumbnail = generateFastThumbnail(PRApp.getContext(), File(filePath)) + Log.d("ocean", "最新图片:$newThumbnail") + if (!newThumbnail.isNullOrEmpty() && filePath != newThumbnail) { + pdfRepository.updateThumbnailPath(filePath, newThumbnail) + } + } } + removed // 这里直接返回移除密码的结果 } + } catch (e: Exception) { + Log.e("ocean", "removePassword 失败", e) + false } + _fileActionEvent.postValue( - FileActionEvent.SetPassword( - FileActionEvent.SetPassword.Status.COMPLETE, + FileActionEvent.RemovePassword( + FileActionEvent.RemovePassword.Status.COMPLETE, success ) ) } } + + fun noticeReloadPdf() { + viewModelScope.launch { + _fileActionEvent.postValue(FileActionEvent.NoticeReload) + } + } } \ No newline at end of file diff --git a/app/src/main/res/color/switch_thumb_color.xml b/app/src/main/res/color/switch_thumb_color.xml new file mode 100644 index 0000000..ba5cb96 --- /dev/null +++ b/app/src/main/res/color/switch_thumb_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/switch_track_color.xml b/app/src/main/res/color/switch_track_color.xml new file mode 100644 index 0000000..32721c9 --- /dev/null +++ b/app/src/main/res/color/switch_track_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/color_inversion.xml b/app/src/main/res/drawable/color_inversion.xml new file mode 100644 index 0000000..c8da4fa --- /dev/null +++ b/app/src/main/res/drawable/color_inversion.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dr_bg_toggle_group.xml b/app/src/main/res/drawable/dr_bg_toggle_group.xml new file mode 100644 index 0000000..8271f6e --- /dev/null +++ b/app/src/main/res/drawable/dr_bg_toggle_group.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/dr_bg_toggle_indicator.xml b/app/src/main/res/drawable/dr_bg_toggle_indicator.xml new file mode 100644 index 0000000..c9580f9 --- /dev/null +++ b/app/src/main/res/drawable/dr_bg_toggle_indicator.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/dr_bg_toggle_left_unselected.xml b/app/src/main/res/drawable/dr_bg_toggle_left_unselected.xml new file mode 100644 index 0000000..006bcf5 --- /dev/null +++ b/app/src/main/res/drawable/dr_bg_toggle_left_unselected.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/dr_bg_toggle_right_selected.xml b/app/src/main/res/drawable/dr_bg_toggle_right_selected.xml new file mode 100644 index 0000000..bb9c6f0 --- /dev/null +++ b/app/src/main/res/drawable/dr_bg_toggle_right_selected.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/page_by_page.xml b/app/src/main/res/drawable/page_by_page.xml new file mode 100644 index 0000000..79ad343 --- /dev/null +++ b/app/src/main/res/drawable/page_by_page.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/read_horizontal.xml b/app/src/main/res/drawable/read_horizontal.xml new file mode 100644 index 0000000..092c7c9 --- /dev/null +++ b/app/src/main/res/drawable/read_horizontal.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/read_vertical.xml b/app/src/main/res/drawable/read_vertical.xml new file mode 100644 index 0000000..8a333a3 --- /dev/null +++ b/app/src/main/res/drawable/read_vertical.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/activity_pdf_view.xml b/app/src/main/res/layout/activity_pdf_view.xml index 9bcd387..3ebd491 100644 --- a/app/src/main/res/layout/activity_pdf_view.xml +++ b/app/src/main/res/layout/activity_pdf_view.xml @@ -44,12 +44,13 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" - android:background="@color/white"> + android:gravity="center"> + android:layout_height="match_parent" + android:background="@color/grey" /> diff --git a/app/src/main/res/layout/dialog_list_more.xml b/app/src/main/res/layout/dialog_list_more.xml index 2f780f7..f344cdc 100644 --- a/app/src/main/res/layout/dialog_list_more.xml +++ b/app/src/main/res/layout/dialog_list_more.xml @@ -246,7 +246,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:fontFamily="@font/poppins_medium" android:text="@string/duplicate_file" android:textColor="@color/grey_text_color" android:textSize="16sp" /> @@ -270,7 +269,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:fontFamily="@font/poppins_medium" android:text="@string/set_password" android:textColor="@color/grey_text_color" android:textSize="16sp" /> @@ -292,7 +290,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:fontFamily="@font/poppins_medium" android:text="@string/delete_file" android:textColor="@color/grey_text_color" android:textSize="16sp" /> diff --git a/app/src/main/res/layout/dialog_view_model.xml b/app/src/main/res/layout/dialog_view_model.xml index 9a6500e..0dca78f 100644 --- a/app/src/main/res/layout/dialog_view_model.xml +++ b/app/src/main/res/layout/dialog_view_model.xml @@ -1,5 +1,6 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 02223d8..c13e7e1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -15,4 +15,5 @@ #2c2c2c #666666 #80FFD699 + #636366 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d35676..2139ca4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -97,4 +97,8 @@ Duplicate file created failed Processing… View Model + Page by page + color inversion + Horizontal + Vertical \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8a15370..5ec9b1f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,7 +17,7 @@ true - + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/switch_button_attrs.xml b/app/src/main/res/values/switch_button_attrs.xml new file mode 100644 index 0000000..da6c1ac --- /dev/null +++ b/app/src/main/res/values/switch_button_attrs.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f45de91..aea20b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ swiperefreshlayout = "1.1.0" recyclerview = "1.4.0" protoliteWellKnownTypes = "18.0.1" material = "1.12.0" +composeMaterial3 = "1.5.1" [libraries] androidpdfviewer = { module = "com.github.marain87:AndroidPdfViewer", version.ref = "androidpdfviewer" } @@ -40,6 +41,7 @@ androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview" pdfbox-android = { module = "com.tom-roush:pdfbox-android", version.ref = "pdfboxAndroid" } protolite-well-known-types = { group = "com.google.firebase", name = "protolite-well-known-types", version.ref = "protoliteWellKnownTypes" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }