Merge pull request #178 from TouchInstinct/feature/base-web-view

Feature/base web view
This commit is contained in:
Aksenov Vladimir 2020-10-19 13:05:09 +03:00 committed by GitHub
commit 887683f270
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 433 additions and 0 deletions

View File

@ -63,6 +63,7 @@ gradle.ext.roboswag = [
'base-map',
'yandex-map',
'google-map',
'webview',
'encrypted-shared-prefs'
]

1
webview/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

44
webview/build.gradle Normal file
View File

@ -0,0 +1,44 @@
apply from: "../android-configs/lib-config.gradle"
apply plugin: 'kotlin-android'
android {
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(":views")
implementation project(":kotlin-extensions")
implementation "com.google.android.material:material"
implementation "androidx.constraintlayout:constraintlayout"
implementation "androidx.core:core-ktx"
constraints {
implementation("com.google.android.material:material") {
version {
require '1.0.0'
}
}
implementation("androidx.constraintlayout:constraintlayout") {
version {
require '2.0.0-beta4'
}
}
implementation("androidx.core:core-ktx") {
version {
require '1.3.1'
}
}
implementation("org.jetbrains.kotlin:kotlin-stdlib") {
version {
require '1.3.0'
}
}
}
}
repositories {
mavenCentral()
}

View File

@ -0,0 +1,2 @@
<manifest
package="ru.touchin.roboswag.webview"/>

View File

@ -0,0 +1,41 @@
package ru.touchin.roboswag.webview.web_view
import android.webkit.ConsoleMessage
import android.webkit.JsPromptResult
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
open class BaseChromeWebViewClient(
private val onJsConfirm: (() -> Unit)? = null,
private val onJsAlert: (() -> Unit)? = null,
private val onJsPrompt: ((String?) -> Unit)? = null,
private val onJsError: ((error: ConsoleMessage) -> Unit)? = null
) : WebChromeClient() {
override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
onJsConfirm?.invoke()
result?.confirm()
return true
}
override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
onJsAlert?.invoke()
result?.confirm()
return true
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
if (consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
onJsError?.invoke(consoleMessage)
}
return super.onConsoleMessage(consoleMessage)
}
override fun onJsPrompt(view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult?): Boolean {
onJsPrompt?.invoke(defaultValue)
result?.confirm()
return true
}
}

View File

@ -0,0 +1,141 @@
package ru.touchin.roboswag.webview.web_view
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.webkit.ConsoleMessage
import android.webkit.WebView
import androidx.core.content.withStyledAttributes
import androidx.core.widget.TextViewCompat
import ru.touchin.extensions.setOnRippleClickListener
import ru.touchin.roboswag.views.widget.Switcher
import ru.touchin.roboswag.webview.R
import ru.touchin.roboswag.webview.databinding.BaseWebViewBinding
open class BaseWebView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int = 0
) : Switcher(context, attrs), WebViewCallback {
private val binding = BaseWebViewBinding.inflate(LayoutInflater.from(context), this)
var onWebViewLoaded: (() -> Unit)? = null
var onWebViewRepeatButtonClicked: (() -> Unit)? = null
var onWebViewScrolled: ((WebView, Int, Int) -> Unit)? = null
var onCookieLoaded: ((cookies: Map<String, String>) -> Unit)? = null
var onJsConfirm: (() -> Unit)? = null
var onJsAlert: (() -> Unit)? = null
var onJsPrompt: ((defaultValue: String?) -> Unit)? = null
var onJsError: ((error: ConsoleMessage) -> Unit)? = null
var isPullToRefreshEnable = false
set(value) {
binding.pullToRefresh.isEnabled = value
binding.pullToRefresh.isRefreshing = false
field = value
}
var isRedirectEnable = false
init {
binding.pullToRefresh.isEnabled = isPullToRefreshEnable
binding.apply {
context.withStyledAttributes(attrs, R.styleable.BaseWebView, defStyleAttr, 0) {
if (hasValue(R.styleable.BaseWebView_errorTextAppearance)) {
TextViewCompat.setTextAppearance(errorText, getResourceId(R.styleable.BaseWebView_errorTextAppearance, 0))
}
if (hasValue(R.styleable.BaseWebView_repeatButtonTextAppearance)) {
TextViewCompat.setTextAppearance(errorRepeatButton, getResourceId(R.styleable.BaseWebView_repeatButtonTextAppearance, 0))
}
if (hasValue(R.styleable.BaseWebView_progressBarTintColor)) {
progressBar.indeterminateTintList = ColorStateList.valueOf(getColor(R.styleable.BaseWebView_progressBarTintColor, Color.BLACK))
}
if (hasValue(R.styleable.BaseWebView_repeatButtonBackground)) {
errorRepeatButton.setBackgroundResource(getResourceId(R.styleable.BaseWebView_repeatButtonBackground, 0))
}
if (hasValue(R.styleable.BaseWebView_screenBackground)) {
setBackgroundResource(getResourceId(R.styleable.BaseWebView_screenBackground, 0))
}
if (hasValue(R.styleable.BaseWebView_errorText)) {
errorText.text = getString(R.styleable.BaseWebView_errorText)
}
if (hasValue(R.styleable.BaseWebView_repeatButtonText)) {
errorRepeatButton.text = getString(R.styleable.BaseWebView_repeatButtonText)
}
}
pullToRefresh.setOnRefreshListener {
webView.reload()
}
errorRepeatButton.setOnRippleClickListener {
onWebViewRepeatButtonClicked?.invoke()
}
webView.onScrollChanged = { scrollX, scrollY, _, _ ->
onWebViewScrolled?.invoke(binding.webView, scrollX, scrollY)
}
setWebViewPreferences()
}
}
override fun onStateChanged(newState: WebViewLoadingState) {
when {
newState == WebViewLoadingState.LOADED -> {
onWebViewLoaded?.invoke()
binding.pullToRefresh.isRefreshing = false
showChild(R.id.pull_to_refresh)
}
newState == WebViewLoadingState.LOADING
&& !binding.pullToRefresh.isRefreshing -> {
showChild(R.id.progress_bar)
}
newState == WebViewLoadingState.ERROR -> {
showChild(R.id.error_layout)
}
}
}
override fun onOverrideUrlLoading(url: String?): Boolean = isRedirectEnable
override fun onPageCookiesLoaded(cookies: Map<String, String>) {
onCookieLoaded?.invoke(cookies)
}
fun setBaseWebViewClient(isSSlPinningEnable: Boolean = false) {
binding.webView.webViewClient = BaseWebViewClient(this, isSSlPinningEnable)
binding.webView.webChromeClient = BaseChromeWebViewClient(onJsConfirm, onJsAlert, onJsPrompt, onJsError)
}
fun getWebView() = binding.webView
fun loadUrl(url: String?) {
binding.webView.loadUrl(url)
}
fun setState(newState: WebViewLoadingState) {
onStateChanged(newState)
}
fun setOnWebViewDisplayedContentAction(action: () -> Unit) {
binding.webView.onWebViewDisplayedContent = action
}
@SuppressLint("SetJavaScriptEnabled")
open fun setWebViewPreferences() {
binding.webView.apply {
scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
with(settings) {
loadsImagesAutomatically = true
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
}
}
}
}

View File

@ -0,0 +1,89 @@
package ru.touchin.roboswag.webview.web_view
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Handler
import android.os.Looper
import android.webkit.CookieManager
import android.webkit.SslErrorHandler
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.os.postDelayed
open class BaseWebViewClient(private val callback: WebViewCallback, private val isSslPinningEnable: Boolean) : WebViewClient() {
companion object {
private const val WEB_VIEW_TIMEOUT_MS = 30 * 1000L // 30 sec
}
private var isError = false
private var isTimeout = true
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
isError = false
callback.onStateChanged(WebViewLoadingState.LOADING)
Looper.myLooper()?.let { looper ->
val handler = Handler(looper)
handler.postDelayed(WEB_VIEW_TIMEOUT_MS) {
if (isTimeout) {
isError = true
pageFinished()
}
isTimeout = true
}
}
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
isTimeout = false
callback.onPageCookiesLoaded(CookieManager.getInstance().getCookie(url).processCookies())
pageFinished()
}
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
return !callback.onOverrideUrlLoading(url) && view.originalUrl != null
}
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
if (isSslPinningEnable) {
super.onReceivedSslError(view, handler, error)
isError = true
callback.onStateChanged(WebViewLoadingState.ERROR)
} else {
handler.proceed()
}
}
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
if (!(error.errorCode == -10 && "about:blank" == request.url.toString())) {
isError = true
}
pageFinished()
}
private fun pageFinished() {
callback.onStateChanged(if (isError) WebViewLoadingState.ERROR else WebViewLoadingState.LOADED)
}
private fun String.processCookies(): Map<String, String> {
val cookiesMap = mutableMapOf<String, String>()
this.split(";")
.forEach { cookie ->
val splittedCookie = cookie.trim().split("=")
cookiesMap[splittedCookie.first()] = splittedCookie.last()
}
return cookiesMap
}
}
enum class WebViewLoadingState {
LOADING,
ERROR,
LOADED
}

View File

@ -0,0 +1,31 @@
package ru.touchin.roboswag.webview.web_view
import android.content.Context
import android.util.AttributeSet
import android.webkit.WebView
class CustomWebView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
var onWebViewDisplayedContent: (() -> Unit)? = null
var onScrollChanged: ((Int, Int, Int, Int) -> Unit)? = null
// https://stackoverflow.com/a/14678910
override fun invalidate() {
super.invalidate()
if (contentHeight > 0) {
onWebViewDisplayedContent?.invoke()
}
}
override fun onScrollChanged(scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY)
onScrollChanged?.invoke(scrollX, scrollY, oldScrollX, oldScrollY)
}
}

View File

@ -0,0 +1,11 @@
package ru.touchin.roboswag.webview.web_view
interface WebViewCallback {
fun onStateChanged(newState: WebViewLoadingState)
fun onOverrideUrlLoading(url: String?): Boolean
fun onPageCookiesLoaded(cookies: Map<String, String>)
}

View File

@ -0,0 +1,58 @@
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/pull_to_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ru.touchin.roboswag.webview.web_view.CustomWebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center" />
<LinearLayout
android:id="@+id/error_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:textColor="@android:color/black"
tools:text="Error text" />
<TextView
android:id="@+id/error_repeat_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
tools:text="Повторить" />
</LinearLayout>
</merge>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BaseWebView">
<attr name="screenBackground" format="reference"/>
<attr name="errorText" format="string"/>
<attr name="repeatButtonText" format="string"/>
<attr name="repeatButtonBackground" format="reference"/>
<attr name="repeatButtonTextAppearance" format="reference"/>
<attr name="errorTextAppearance" format="reference"/>
<attr name="progressBarTintColor" format="color"/>
</declare-styleable>
</resources>