From 918661be2d25dab95d72dcd4e86027d68201432e Mon Sep 17 00:00:00 2001 From: davidoskky Date: Tue, 22 Dec 2020 20:06:38 +0100 Subject: [PATCH] Expand images on tap (#315) * Detect click on images in WebView * First stub of the fragment to show the image in full screen * Scale image dimension to fit the display * Hide toolbar from Image view * Add back button to the Image view * Open one image on tap * Allow zooming on images * Revert to using Toolbar for navigation. * Remove vibration when opening the Image view * Do not open links associated with images * Send all images in the webpage to the Image fragment. * Change image on swipe * Store article images in cache in background. * Use PhotoView in place of WebView to display images. Implemented a pager to swipe through images. * Removed debugging logging. --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 3 + .../bou/readerforselfoss/ImageActivity.kt | 52 +++++++++++++ .../api/selfoss/SelfossModels.kt | 35 +++++++++ .../readerforselfoss/background/background.kt | 1 + .../fragments/ArticleFragment.kt | 74 +++++++++++++++++-- .../fragments/ImageFragment.kt | 49 ++++++++++++ .../utils/glide/GlideUtils.kt | 10 +++ app/src/main/res/layout/activity_image.xml | 33 +++++++++ app/src/main/res/layout/fragment_image.xml | 16 ++++ 10 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/ImageActivity.kt create mode 100644 app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ImageFragment.kt create mode 100644 app/src/main/res/layout/activity_image.xml create mode 100644 app/src/main/res/layout/fragment_image.xml diff --git a/app/build.gradle b/app/build.gradle index 92787ac..a456f57 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -138,6 +138,9 @@ dependencies { // Pager implementation 'me.relex:circleindicator:2.0.0@aar' + //PhotoView + implementation 'com.github.chrisbanes:PhotoView:2.0.0' + implementation 'androidx.core:core-ktx:1.1.0-beta01' implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5575c92..748ff1b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -62,6 +62,9 @@ + + + private var position : Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_image) + + setSupportActionBar(toolBar) + supportActionBar?.setDisplayShowTitleEnabled(false) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + allImages = intent.getStringArrayListExtra("allImages") + position = intent.getIntExtra("position", 0) + + pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager) + pager.currentItem = position + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + } + + return super.onOptionsItemSelected(item) + } + + private inner class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getCount(): Int { + return allImages.size + } + + override fun getItem(position: Int): ImageFragment { + return ImageFragment.newInstance(allImages[position]) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt index a0c33e0..516c78f 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/api/selfoss/SelfossModels.kt @@ -5,9 +5,14 @@ import android.net.Uri import android.os.Parcel import android.os.Parcelable import android.text.Html +import android.webkit.URLUtil +import org.jsoup.Jsoup import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions import com.google.gson.annotations.SerializedName private fun constructUrl(config: Config?, path: String, file: String?): String { @@ -128,6 +133,36 @@ data class Item( return constructUrl(config, "thumbnails", thumbnail) } + fun getImages() : ArrayList { + var allImages = ArrayList() + + for ( image in Jsoup.parse(content).getElementsByTag("img")) { + allImages.add(image.attr("src")) + } + return allImages + } + + fun preloadImages(context: Context) : Boolean { + val imageUrls = this.getImages() + + val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) + + + try { + for (url in imageUrls) { + if ( URLUtil.isValidUrl(url)) { + val image = Glide.with(context).asBitmap() + .apply(glideOptions) + .load(url).submit().get() + } + } + } catch (e : Error) { + return false + } + + return true + } + fun getTitleDecoded(): String { return Html.fromHtml(title).toString() } diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt index 1bcddb5..a1ce7dc 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/background/background.kt @@ -105,6 +105,7 @@ class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(con notificationManager.notify(2, newItemsNotification.build()) } } + apiItems.map {it.preloadImages(context)} } Timer("", false).schedule(4000) { notificationManager.cancel(1) diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ArticleFragment.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ArticleFragment.kt index 7bc6f5b..f5b10a9 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ArticleFragment.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ArticleFragment.kt @@ -1,28 +1,28 @@ package apps.amine.bou.readerforselfoss.fragments import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.content.res.ColorStateList import android.content.res.TypedArray +import android.graphics.Bitmap import android.graphics.Typeface import android.graphics.drawable.ColorDrawable +import android.net.Uri import android.os.Build import android.os.Bundle import android.preference.PreferenceManager -import android.view.InflateException +import android.view.* +import android.webkit.* import androidx.browser.customtabs.CustomTabsIntent import com.google.android.material.floatingactionbutton.FloatingActionButton import androidx.fragment.app.Fragment import androidx.core.content.ContextCompat import androidx.core.widget.NestedScrollView -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.webkit.WebSettings import androidx.appcompat.app.AlertDialog import androidx.core.content.res.ResourcesCompat import androidx.room.Room +import apps.amine.bou.readerforselfoss.ImageActivity import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent @@ -39,6 +39,7 @@ import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth +import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.openItemUrl @@ -46,6 +47,7 @@ import apps.amine.bou.readerforselfoss.utils.shareLink import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.succeeded import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.github.rubensousa.floatingtoolbar.FloatingToolbar import kotlinx.android.synthetic.main.fragment_article.view.* @@ -54,6 +56,8 @@ import retrofit2.Callback import retrofit2.Response import java.net.MalformedURLException import java.net.URL +import java.util.concurrent.ExecutionException +import kotlin.collections.ArrayList import kotlin.concurrent.thread class ArticleFragment : Fragment() { @@ -66,6 +70,7 @@ class ArticleFragment : Fragment() { private lateinit var contentSource: String private lateinit var contentImage: String private lateinit var contentTitle: String + private lateinit var allImages : ArrayList private lateinit var editor: SharedPreferences.Editor private lateinit var fab: FloatingActionButton private lateinit var appColors: AppColors @@ -117,6 +122,7 @@ class ArticleFragment : Fragment() { contentTitle = allItems[pageNumber.toInt()].getTitleDecoded() contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!) contentSource = allItems[pageNumber.toInt()].sourceAndDateText() + allImages = allItems[pageNumber.toInt()].getImages() prefs = PreferenceManager.getDefaultSharedPreferences(activity) editor = prefs.edit() @@ -411,6 +417,47 @@ class ArticleFragment : Fragment() { rootView!!.webcontent.settings.loadWithOverviewMode = true rootView!!.webcontent.settings.javaScriptEnabled = false + rootView!!.webcontent.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean { + if (rootView!!.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + rootView!!.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + } + return true + } + + override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { + val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) + if (url.toLowerCase().contains(".jpg") || url.toLowerCase().contains(".jpeg")) { + try { + val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) + }catch ( e : ExecutionException) {} + } + else if (url.toLowerCase().contains(".png")) { + try { + val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) + }catch ( e : ExecutionException) {} + } + else if (url.toLowerCase().contains(".webp")) { + try { + val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() + return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) + }catch ( e : ExecutionException) {} + } + + return super.shouldInterceptRequest(view, url) + } + } + + val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent?): Boolean { + return performClick() + } + }) + + rootView!!.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING @@ -526,5 +573,20 @@ class ArticleFragment : Fragment() { } } + fun performClick(): Boolean { + if (rootView!!.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || + rootView!!.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + + val position : Int = allImages.indexOf(rootView!!.webcontent.hitTestResult.extra) + + val intent = Intent(activity, ImageActivity::class.java) + intent.putExtra("allImages", allImages) + intent.putExtra("position", position) + startActivity(intent) + return false + } + return false + } + } diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ImageFragment.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ImageFragment.kt new file mode 100644 index 0000000..57e26c8 --- /dev/null +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/fragments/ImageFragment.kt @@ -0,0 +1,49 @@ +package apps.amine.bou.readerforselfoss.fragments + +import android.os.Bundle +import android.view.* +import androidx.fragment.app.Fragment +import apps.amine.bou.readerforselfoss.R +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import kotlinx.android.synthetic.main.fragment_image.view.* + +class ImageFragment : Fragment() { + + private lateinit var imageUrl : String + private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + imageUrl = arguments!!.getString("imageUrl") + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view : View = inflater.inflate(R.layout.fragment_image, container, false) + + view.photoView.visibility = View.VISIBLE + Glide.with(activity) + .asBitmap() + .apply(glideOptions) + .load(imageUrl) + .into(view.photoView) + + return view + } + + companion object { + private const val ARG_IMAGE = "imageUrl" + + fun newInstance( + imageUrl : String + ): ImageFragment { + val fragment = ImageFragment() + val args = Bundle() + args.putString(ARG_IMAGE, imageUrl) + fragment.arguments = args + return fragment + } + } +} \ No newline at end of file diff --git a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/glide/GlideUtils.kt b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/glide/GlideUtils.kt index ea29a70..9183126 100644 --- a/app/src/main/java/apps/amine/bou/readerforselfoss/utils/glide/GlideUtils.kt +++ b/app/src/main/java/apps/amine/bou/readerforselfoss/utils/glide/GlideUtils.kt @@ -14,6 +14,9 @@ import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.BitmapImageViewTarget +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) = Glide.with(this) @@ -56,4 +59,11 @@ fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuild } val glideUrl = GlideUrl(url, builder.build()) return this.load(glideUrl) +} + +fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream { + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(compressFormat, 80, byteArrayOutputStream) + val bitmapData: ByteArray = byteArrayOutputStream.toByteArray() + return ByteArrayInputStream(bitmapData) } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image.xml b/app/src/main/res/layout/activity_image.xml new file mode 100644 index 0000000..d5e50c4 --- /dev/null +++ b/app/src/main/res/layout/activity_image.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_image.xml b/app/src/main/res/layout/fragment_image.xml new file mode 100644 index 0000000..ae2c7a8 --- /dev/null +++ b/app/src/main/res/layout/fragment_image.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file