Compare commits

...

2 Commits

Author SHA1 Message Date
49a610deee feat: 动态java方法注册 2023-10-11 19:41:28 +08:00
61f43d53b2 feat: 文章详情WebView预加载 2023-10-11 15:52:35 +08:00
10 changed files with 182 additions and 73 deletions

View File

@ -352,9 +352,9 @@ RE.ImageClickListener = function() {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link"|| img.className == "poster") continue;
window.imagelistener.imageArr(img.src);
window.NativeCallBack.invokeMethod("imageArr", img.src);
img.onclick = function() {
window.imagelistener.imageClick(this.src);
window.NativeCallBack.invokeMethod("imageClick", this.src);
}
}
}

View File

@ -43,8 +43,10 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Copyright (C) 2017 Wasabeef
@ -77,6 +79,8 @@ public class RichEditor extends WebView {
private EmptyCallback mInitialLayoutCallback;
private Map<String, DynamicJsInterface> mDynamicJsInterfaces = new HashMap<>();
private String mCurrentContent = "";
public enum Type {
@ -605,6 +609,14 @@ public class RichEditor extends WebView {
exec("javascript:RE.formatBlock();");
}
public void registerDynamicJsInterface(String method, DynamicJsInterface jsInterface) {
mDynamicJsInterfaces.put(method, jsInterface);
}
public void unregisterDynamicJsInterface(String method) {
mDynamicJsInterfaces.remove(method);
}
/**
* 调用 JS 方法,告诉网页端该 url 对应视频播放的进度
*/
@ -862,6 +874,14 @@ public class RichEditor extends WebView {
public void logMtaEvent(String event) {
// do nothing, mta is deprecated
}
@JavascriptInterface
public void invokeMethod(String method, String data) {
DynamicJsInterface jsInterface = mDynamicJsInterfaces.get(method);
if (jsInterface != null) {
jsInterface.invoke(data);
}
}
}
@Override
@ -874,4 +894,8 @@ public class RichEditor extends WebView {
mInitialLayoutCallback = null;
}
}
public interface DynamicJsInterface {
void invoke(String data);
}
}

View File

@ -62,7 +62,7 @@ class CommunityHomeFragment : LazyFragment() {
override fun onFragmentFirstVisible() {
ArticleWebCacheManager.init(requireContext())
ArticleWebCacheManager.init(requireContext().applicationContext)
mViewModel = viewModelProvider()

View File

@ -8,14 +8,12 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager.LayoutParams
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.R
import com.gh.gamecenter.adapter.viewholder.UnAvaliableWebviewViewHolder
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.databinding.TabItemBinding
import com.gh.gamecenter.common.utils.dip2px
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.common.utils.visibleIf
import com.gh.gamecenter.core.utils.MtaHelper
@ -53,6 +51,7 @@ class ArticleDetailAdapter(
ITEM_ARTICLE_DETAIL -> {
val isWebViewInstalled = PackageUtils.checkWebViewIsAvailable(mContext)
if (isWebViewInstalled) {
ArticleDetailContentViewHolder.wvLoadTimeInMills = System.currentTimeMillis()
val binding: ItemArticleDetailContentBinding =
ItemArticleDetailContentBinding.inflate(mLayoutInflater, parent, false)
ArticleDetailContentViewHolder(binding, mViewModel).apply { articleDetailVH = this }

View File

@ -5,10 +5,10 @@ import android.app.Activity
import android.graphics.Bitmap
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.Log
import android.view.Gravity
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.widget.ImageView
import android.widget.LinearLayout
@ -42,6 +42,33 @@ class ArticleDetailContentViewHolder(
var viewModel: ArticleDetailViewModel
) : RecyclerView.ViewHolder(binding.root) {
companion object {
var wvLoadTimeInMills = 0L
}
val richEditor = ArticleWebCacheManager
.attachToRichEditor(binding.richEditorContainer)
.apply {
setInputEnabled(false)
setPadding(16, 4, 16, 4)
enableForceDark(DarkModeUtils.isDarkModeOn(binding.root.context))
setTransparentBackground()
setLayoutCallback { viewModel.articleRenderedLiveData.postValue(true) }
setPageFinishedListener {
Log.d("ArticleDetail", "pageFinished->${System.currentTimeMillis() - wvLoadTimeInMills}")
viewModel.articlePageFinishedLiveData.postValue(true)
}
setChromeClientListener(object : RichEditor.WebChromeClientListener {
override fun onPageFinished(view: WebView?, url: String?) {
Log.d("ArticleDetail", "onPageFinished: $url->${System.currentTimeMillis() - wvLoadTimeInMills}")
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return DefaultUrlHandler.interceptUrl(binding.root.context, url ?: "", "帖子详情")
}
})
}
private var mEntrance = ""
val articleImgUrlList = ArrayList<String>()
@ -59,11 +86,41 @@ class ArticleDetailContentViewHolder(
titleTv.setTextColor(R.color.text_title.toColor(binding.root.context))
gameName.setTextColor(R.color.text_subtitleDesc.toColor(binding.root.context))
richEditor.enableForceDark(DarkModeUtils.isDarkModeOn(binding.root.context))
richEditor.setTransparentBackground()
richEditor.setInputEnabled(false)
richEditor.setPadding(16, 4, 16, 4)
richEditor.addJavascriptInterface(JsInterface(article.status ?: ""), "imagelistener")
richEditor.registerDynamicJsInterface("imageArr") { url ->
val defUrl = url.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]
if (!articleImgUrlList.contains(defUrl) && !url.contains("web_load_dfimg_icon.png")) {
articleImgUrlList.add(defUrl)
}
}
richEditor.registerDynamicJsInterface("imageClick") { url ->
when {
url.contains("web_load_dfimg_icon.png") -> {
runOnUiThread { richEditor.replaceAllDfImageExcludeGif() }
}
else -> {
val status = article.status ?: ""
clickToastByStatus(status) {
var current = 0
var i = 0
val size = articleImgUrlList.size
while (i < size) {
if (url.contains(articleImgUrlList.get(i))) {
current = i
}
i++
}
val intent = ImageViewerActivity.getIntent(
binding.root.context, articleImgUrlList, current,
mEntrance + "+(帖子详情[" + binding.titleTv.text.toString() + "])"
)
(binding.root.context as Activity).startActivityForResult(
intent,
ImageViewerActivity.REQUEST_FOR_VIEWED_IMAGE
)
}
}
}
}
richEditor.addJavascriptInterface(
OnLinkClickListener(
root.context,
@ -73,26 +130,6 @@ class ArticleDetailContentViewHolder(
"帖子详情"
), "OnLinkClickListener"
)
richEditor.setLayoutCallback(object : EmptyCallback {
override fun onCallback() {
viewModel.articleRenderedLiveData.postValue(true)
}
})
richEditor.setPageFinishedListener {
viewModel.articlePageFinishedLiveData.postValue(true)
}
richEditor.setChromeClientListener(object : RichEditor.WebChromeClientListener {
override fun onPageFinished(view: WebView?, url: String?) {
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return DefaultUrlHandler.interceptUrl(binding.root.context, url ?: "", "帖子详情")
}
})
richEditor.setWebResourceRequestInterceptor { view, url ->
ArticleWebCacheManager.shouldInterceptRequest(view, url)
}
approvalStatusTv.goneIf(article.status == "pass")
statusContainer.goneIf(article.status == "pass")
@ -325,7 +362,7 @@ class ArticleDetailContentViewHolder(
* 回调列表视频播放结束时的时间
*/
fun onVideoPlayedCallback(url: String, position: Int) {
binding.richEditor.onVideoPlayedCallback(url, position)
richEditor.onVideoPlayedCallback(url, position)
}
fun updateFollowBtn(isFollowed: Boolean) {
@ -349,7 +386,7 @@ class ArticleDetailContentViewHolder(
fun imageClick(url: String) {
when {
url.contains("web_load_dfimg_icon.png") -> {
runOnUiThread { binding.richEditor.replaceAllDfImageExcludeGif() }
runOnUiThread { richEditor.replaceAllDfImageExcludeGif() }
}
// url.contains(RichEditor.IMAGE_FLAG_THUMBNAIL) -> {
// runOnUiThread { binding.richEditor.replaceDfImageByUrl(url) }
@ -386,4 +423,14 @@ class ArticleDetailContentViewHolder(
}
}
}
fun destroy() {
richEditor.setLayoutCallback(null)
richEditor.removeJavascriptInterface("imagelistener")
richEditor.removeJavascriptInterface("OnLinkClickListener")
richEditor.setChromeClientListener(null)
richEditor.setContentOwner(false)
richEditor.setPageFinishedListener(null)
ArticleWebCacheManager.detachFromRichEditor(binding.richEditorContainer)
}
}

View File

@ -26,7 +26,6 @@ import com.gh.gamecenter.common.baselist.LoadType
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.entity.AdditionalParamsEntity
import com.gh.gamecenter.feature.entity.CommentEntity
import com.gh.gamecenter.common.entity.CommunityEntity
import com.gh.gamecenter.common.entity.NormalShareEntity
import com.gh.gamecenter.common.eventbus.EBReuse
@ -41,6 +40,7 @@ import com.gh.gamecenter.eventbus.EBDeleteCommentDetail
import com.gh.gamecenter.eventbus.EBDeleteDetail
import com.gh.gamecenter.eventbus.EBTopCommunityChanged
import com.gh.gamecenter.feature.entity.ArticleDraftEntity
import com.gh.gamecenter.feature.entity.CommentEntity
import com.gh.gamecenter.feature.entity.Permissions
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.qa.article.edit.ArticleEditActivity
@ -119,11 +119,11 @@ class ArticleDetailFragment : BaseCommentFragment<CommentItemData, ArticleDetail
mAdapter?.articleDetailVH?.run {
if (articleImgUrlList.size > 0) {
if (imageSet.size == articleImgUrlList.size) {
binding.richEditor.replaceAllDfImage()
richEditor.replaceAllDfImage()
} else {
for (i in imageSet) {
val url = articleImgUrlList[i.toInt()]
binding.richEditor.replaceDfImageByUrl(url)
richEditor.replaceDfImageByUrl(url)
}
}
}
@ -165,6 +165,9 @@ class ArticleDetailFragment : BaseCommentFragment<CommentItemData, ArticleDetail
override fun onDestroyView() {
super.onDestroyView()
mAdapter?.articleDetailVH?.destroy()
if (mViewModel.detailEntity != null) {
HistoryHelper.insertArticleEntity(mViewModel.detailEntity!!)

View File

@ -2,8 +2,11 @@ package com.gh.gamecenter.qa.article.detail
import android.annotation.SuppressLint
import android.content.Context
import android.content.MutableContextWrapper
import android.view.ViewGroup
import android.webkit.WebResourceResponse
import android.webkit.WebView
import com.gh.common.view.RichEditor
import com.gh.gamecenter.common.utils.EnvHelper
import com.gh.gamecenter.core.utils.MD5Utils
import io.reactivex.Single
@ -18,6 +21,7 @@ import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Streaming
import retrofit2.http.Url
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
@ -25,23 +29,26 @@ import java.util.concurrent.TimeUnit
/**
* 社区文章详情-Web资源缓存管理类
*/
@SuppressLint("StaticFieldLeak")
object ArticleWebCacheManager {
private lateinit var cacheDir: File
private lateinit var cacheRichEditor: RichEditor
private var isInit = false
fun init(context: Context) {
init(File(context.cacheDir, "article/web"))
}
@SuppressLint("CheckResult")
fun init(cacheDir: File) {
fun init(applicationContext: Context) {
if (isInit) return
isInit = true
this.cacheDir = cacheDir.apply {
this.cacheRichEditor = RichEditor(
MutableContextWrapper(applicationContext)
)
this.cacheDir = File(applicationContext.cacheDir, "article/web").apply {
if (!exists()) mkdirs()
}
@ -92,6 +99,25 @@ object ArticleWebCacheManager {
.subscribe({}, {})
}
fun attachToRichEditor(parent: ViewGroup): RichEditor {
val richEditor = this.cacheRichEditor.apply {
(this.context as MutableContextWrapper).baseContext = parent.context
setWebResourceRequestInterceptor { view, url -> shouldInterceptRequest(view, url) }
}
parent.addView(
richEditor,
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
)
return cacheRichEditor
}
fun detachFromRichEditor(parent: ViewGroup) {
this.cacheRichEditor.setHtml("", false)
this.cacheRichEditor.setWebResourceRequestInterceptor(null)
(this.cacheRichEditor.context as MutableContextWrapper).baseContext = parent.context.applicationContext
parent.removeView(this.cacheRichEditor)
}
/**
* 拦截WebView的资源请求并返回URL对应的缓存数据
*/

View File

@ -108,11 +108,11 @@ class NewQuestionDetailFragment :
mAdapter?.questionDetailVH?.run {
if (questionImgUrlList.size > 0) {
if (imageSet.size == questionImgUrlList.size) {
binding.richEditor.replaceAllDfImage()
richEditor.replaceAllDfImage()
} else {
for (i in imageSet) {
val url = questionImgUrlList[i.toInt()]
binding.richEditor.replaceDfImageByUrl(url)
richEditor.replaceDfImageByUrl(url)
}
}
}
@ -742,6 +742,11 @@ class NewQuestionDetailFragment :
mBinding.root.setBackgroundColor(Color.TRANSPARENT)
}
override fun onDestroyView() {
super.onDestroyView()
mAdapter?.questionDetailVH?.destroy()
}
override fun onDarkModeChanged() {
super.onDarkModeChanged()
mAdapter?.let { it.notifyItemRangeChanged(0, it.itemCount) }

View File

@ -6,7 +6,6 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
@ -38,43 +37,39 @@ class QuestionDetailContentViewHolder(
var viewModel: NewQuestionDetailViewModel
) : RecyclerView.ViewHolder(binding.root) {
val richEditor = ArticleWebCacheManager
.attachToRichEditor(binding.richEditorContainer)
.apply {
setInputEnabled(false)
setPadding(16, 4, 16, 4)
enableForceDark(DarkModeUtils.isDarkModeOn(binding.root.context))
setTransparentBackground()
setLayoutCallback { viewModel.questionRenderedLiveData.postValue(true) }
setPageFinishedListener { viewModel.questionPageFinishedLiveData.postValue(true) }
setChromeClientListener(object : RichEditor.WebChromeClientListener {
override fun onPageFinished(view: WebView?, url: String?) {}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return DefaultUrlHandler.interceptUrl(binding.root.context, url ?: "", "问题详情")
}
})
}
private var mEntrance = ""
val questionImgUrlList = ArrayList<String>()
@SuppressLint("AddJavascriptInterface")
fun bindView(question: QuestionsDetailEntity) {
binding.run {
richEditor.setInputEnabled(false)
richEditor.enableForceDark(DarkModeUtils.isDarkModeOn(binding.root.context))
richEditor.setTransparentBackground()
richEditor.setPadding(16, 4, 16, 4)
richEditor.removeJavascriptInterface("imagelistener")
richEditor.addJavascriptInterface(JsInterface(question.status), "imagelistener")
richEditor.removeJavascriptInterface("OnLinkClickListener")
richEditor.addJavascriptInterface(
OnLinkClickListener(
root.context, question.title
?: "", question.status, mEntrance, "问题详情"
), "OnLinkClickListener"
)
richEditor.setLayoutCallback(object : EmptyCallback {
override fun onCallback() {
viewModel.questionRenderedLiveData.postValue(true)
}
})
richEditor.setPageFinishedListener {
viewModel.questionPageFinishedLiveData.postValue(true)
}
richEditor.setChromeClientListener(object : RichEditor.WebChromeClientListener {
override fun onPageFinished(view: WebView?, url: String?) {
}
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return DefaultUrlHandler.interceptUrl(binding.root.context, url ?: "", "问题详情")
}
})
richEditor.setWebResourceRequestInterceptor { view, url ->
ArticleWebCacheManager.shouldInterceptRequest(view, url)
}
approvalStatusTv.goneIf(question.status == "pass")
statusContainer.goneIf(question.status == "pass")
when (question.status) {
@ -280,7 +275,7 @@ class QuestionDetailContentViewHolder(
* 回调列表视频播放结束时的时间
*/
fun onVideoPlayedCallback(url: String, position: Int) {
binding.richEditor.onVideoPlayedCallback(url, position)
richEditor.onVideoPlayedCallback(url, position)
}
inner class JsInterface(val status: String) {
@ -288,7 +283,7 @@ class QuestionDetailContentViewHolder(
fun imageClick(url: String) {
when {
url.contains("web_load_dfimg_icon.png") -> {
runOnUiThread { binding.richEditor.replaceAllDfImageExcludeGif() }
runOnUiThread { richEditor.replaceAllDfImageExcludeGif() }
}
// url.contains(RichEditor.IMAGE_FLAG_THUMBNAIL) -> {
// runOnUiThread { binding.richEditor.replaceDfImageByUrl(url) }
@ -325,4 +320,14 @@ class QuestionDetailContentViewHolder(
}
}
}
fun destroy() {
richEditor.setLayoutCallback(null)
richEditor.removeJavascriptInterface("imagelistener")
richEditor.removeJavascriptInterface("OnLinkClickListener")
richEditor.setChromeClientListener(null)
richEditor.setContentOwner(false)
richEditor.setPageFinishedListener(null)
ArticleWebCacheManager.detachFromRichEditor(binding.richEditorContainer)
}
}

View File

@ -217,8 +217,8 @@
android:textStyle="bold"
tools:text="这是一个很长很长很长很长很长很长很长很长很长很长的标题" />
<com.gh.common.view.RichEditor
android:id="@+id/richEditor"
<FrameLayout
android:id="@+id/richEditorContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="13dp" />