Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package llc.lookatwhataicando.codeoba.android
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
Expand All @@ -12,15 +13,19 @@ import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -32,15 +37,24 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt

/**
* Test activity with WebView and editable address bar.
Expand Down Expand Up @@ -149,9 +163,18 @@ fun TestWebView(
onWebViewCreated: (WebView) -> Unit = {},
onUrlChanged: (String) -> Unit = {}
) {
AndroidView(
modifier = modifier,
factory = { context ->
var isRefreshing by remember { mutableStateOf(false) }
var pullOffset by remember { mutableFloatStateOf(0f) }
val coroutineScope = rememberCoroutineScope()
val refreshThreshold = with(LocalDensity.current) { 120.dp.toPx() }
val minDragToActivate = with(LocalDensity.current) { 20.dp.toPx() }

Box(modifier = modifier) {
AndroidView(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(0, pullOffset.roundToInt()) },
factory = { context ->
WebView(context).apply {
Log.d("TestWebViewActivity", "Creating WebView")

Expand Down Expand Up @@ -233,6 +256,106 @@ fun TestWebView(
}
}

// Implement pull-to-refresh with touch listener
var downY = 0f
var downX = 0f
var totalDragDistance = 0f
var isDragging = false

setOnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downY = event.y
downX = event.x
totalDragDistance = 0f
isDragging = false
false // Let WebView handle it
}
MotionEvent.ACTION_MOVE -> {
val currentY = event.y
val currentX = event.x
val deltaY = currentY - downY
val deltaX = currentX - downX

// CRITICAL: Always check if we're at the top of the page
// This must be checked on EVERY move, not just when starting
val isAtTop = (view as? WebView)?.scrollY == 0

// Stricter vertical drag detection to avoid drawer conflicts
// Require significantly more vertical than horizontal movement
val isVerticalDrag = kotlin.math.abs(deltaY) > kotlin.math.abs(deltaX) * 2.5f

// Only activate pull-to-refresh after minimum drag threshold
// This prevents conflicts with text selection
val hasMinimumDrag = kotlin.math.abs(deltaY) >= minDragToActivate

if (isAtTop && deltaY > 0 && !isRefreshing && isVerticalDrag && hasMinimumDrag) {
isDragging = true
totalDragDistance = deltaY

// Apply resistance - more aggressive after threshold
val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f
pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f)

true // Consume touch to prevent scrolling
} else if (isDragging && isAtTop) {
// Continue drag ONLY if still at top
// If user scrolled away, stop the pull-to-refresh
totalDragDistance = deltaY
val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f
pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f)
true
} else {
// Stop dragging if we're no longer at top
if (isDragging && !isAtTop) {
isDragging = false
pullOffset = 0f
}
false
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isDragging) {
isDragging = false

// Trigger refresh if threshold met
if (totalDragDistance > refreshThreshold) {
isRefreshing = true
coroutineScope.launch {
(view as? WebView)?.reload()
delay(1000)
isRefreshing = false
}
}

// Animate back
coroutineScope.launch {
val start = pullOffset
val duration = 200L
val startTime = System.currentTimeMillis()

while (pullOffset > 0) {
val elapsed = System.currentTimeMillis() - startTime
val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f)
pullOffset = start * (1f - progress)

if (progress >= 1f) {
pullOffset = 0f
break
}
delay(16)
}
}

true
} else {
false
}
}
else -> false
}
}

Log.d("TestWebViewActivity", "Loading URL: $url")
loadUrl(url)
onWebViewCreated(this)
Expand All @@ -247,4 +370,32 @@ fun TestWebView(
}
}
)

// Refresh indicator
if (isRefreshing || pullOffset > 0) {
// Background overlay to make indicator more visible
Surface(
modifier = Modifier
.align(Alignment.TopCenter)
.offset { IntOffset(0, (pullOffset * 0.3f).roundToInt()) }
.size(56.dp)
.graphicsLayer {
alpha = (pullOffset / refreshThreshold).coerceIn(0.3f, 0.9f)
},
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 4.dp
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(32.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package llc.lookatwhataicando.codeoba.core.ui

import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.util.Log
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* Android WebView with Material 3 PullToRefreshBox integration.
* Uses Compose's built-in pull-to-refresh mechanism with custom nested scroll handling
* to work with AndroidView.WebView.
*
* This provides Chrome-like pull-to-refresh UX with circular indicator.
*/
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun PullToRefreshWebView(
url: String,
modifier: Modifier = Modifier,
onWebViewCreated: ((WebView) -> Unit)? = null
) {
var webView by remember { mutableStateOf<WebView?>(null) }
var canGoBack by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()

// Handle back navigation
BackHandler(enabled = canGoBack) {
webView?.goBack()
}

// PullToRefreshBox provides the pull-to-refresh gesture and indicator
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
coroutineScope.launch {
isRefreshing = true
webView?.reload()
// Simulate refresh duration (WebView doesn't have completion callback)
delay(1000)
isRefreshing = false
}
},
modifier = modifier.fillMaxSize()
) {
// WebView wrapped in Box for proper layout
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
if (0 != (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)) {
WebView.setWebContentsDebuggingEnabled(true)
}
WebView(context).apply {
// CRITICAL: Set layout params to ensure WebView has proper dimensions
layoutParams = android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT
)

// Enable JavaScript and DOM storage
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
@Suppress("DEPRECATION")
databaseEnabled = true
}

// Enable cookies for session persistence
val cookieManager = CookieManager.getInstance()
cookieManager.setAcceptCookie(true)
cookieManager.setAcceptThirdPartyCookies(this, true)

// WebViewClient for navigation handling
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false // Let WebView handle navigation
}

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
canGoBack = view?.canGoBack() ?: false
Log.d("WebView", "Page started: $url")
}

override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
canGoBack = view?.canGoBack() ?: false
Log.d("WebView", "Page finished: $url")
}

@Suppress("OVERRIDE_DEPRECATION")
@Deprecated("Deprecated in Java")
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Log.e("WebView", "Error $errorCode: $description at $failingUrl")
}
}

// WebChromeClient for progress updates
webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
Log.d("WebView", "Loading progress: $newProgress%")
}
}

// Load the initial URL
loadUrl(url)

// Store reference and notify callback
webView = this
onWebViewCreated?.invoke(this)
}
},
update = { view ->
// Update if URL changes (though typically doesn't in this app)
if (view.url != url) {
view.loadUrl(url)
}
}
)
}
}
}
Loading
Loading