基本浏览器模块完成

This commit is contained in:
2025-11-27 11:30:27 +08:00
parent ab03d3166d
commit 204e2c6f0f
10 changed files with 603 additions and 19 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Thu Nov 27 10:54:58 HKT 2025
#Thu Nov 27 03:28:47 GMT 2025
stageCount=2
libraryProject=
baseVersion=15.11
publishVersion=15.11.1
buildCount=0
buildCount=8
baseBetaVersion=15.11.2

View File

@@ -19,7 +19,9 @@
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
android:name=".App"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"

View File

@@ -17,13 +17,14 @@ import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.activities.AboutActivity;
import cc.winboll.studio.winboll.fragments.MainFragment;
import java.util.ArrayList;
import cc.winboll.studio.winboll.fragments.BrowserFragment;
public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActivity {
public static final String TAG = "MainActivity";
MainFragment mMainFragment;
BrowserFragment mBrowserFragment;
@Override
public Activity getActivity() {
@@ -38,11 +39,11 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mMainFragment == null) {
mMainFragment = new MainFragment();
addFragment(mMainFragment);
if (mBrowserFragment == null) {
mBrowserFragment = new BrowserFragment();
addFragment(mBrowserFragment);
}
showFragment(mMainFragment);
showFragment(mBrowserFragment);
}
@Override
@@ -86,11 +87,11 @@ public class MainActivity extends DrawerFragmentActivity implements IWinBoLLActi
super.onItemClick(parent, view, position, id);
switch (position) {
case 0 : {
if (mMainFragment == null) {
mMainFragment = new MainFragment();
addFragment(mMainFragment);
if (mBrowserFragment == null) {
mBrowserFragment = new BrowserFragment();
addFragment(mBrowserFragment);
}
showFragment(mMainFragment);
showFragment(mBrowserFragment);
break;
}
}

View File

@@ -0,0 +1,228 @@
package cc.winboll.studio.winboll.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import cc.winboll.studio.winboll.R;
import cc.winboll.studio.winboll.views.WinBoLLView;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 11:09
* @Describe 浏览器FragmentJava 7 语法完整版)
* 适配Java 7特性移除Lambda/方法引用兼容低版本Android系统
*/
public class BrowserFragment extends Fragment implements View.OnClickListener, WinBoLLView.OnPageStatusListener {
// 控件声明Java 7 成员变量显式声明)
private EditText mEtUrl;
private Button mBtnLoad;
private Button mBtnRefresh;
private Button mBtnStop;
private Button mBtnForward;
private Button mBtnBack;
private ProgressBar mProgressBar;
private WinBoLLView mWinBoLLView;
// 单例创建方法Java 7 静态工厂模式)
public static BrowserFragment newInstance() {
return new BrowserFragment();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
// 加载布局Java 7 显式强转,无菱形语法)
View view = inflater.inflate(R.layout.fragment_browser, container, false);
// 初始化控件
initViews(view);
// 绑定事件
initEvents();
// 初始化WinBoLLView
initWinBoLLView();
return view;
}
/**
* 初始化控件Java 7 显式绑定无Stream简化
*/
private void initViews(View view) {
mEtUrl = (EditText) view.findViewById(R.id.et_url);
mBtnLoad = (Button) view.findViewById(R.id.btn_load);
mBtnRefresh = (Button) view.findViewById(R.id.btn_refresh);
mBtnStop = (Button) view.findViewById(R.id.btn_stop);
mBtnForward = (Button) view.findViewById(R.id.btn_forward);
mBtnBack = (Button) view.findViewById(R.id.btn_back);
mProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
mWinBoLLView = (WinBoLLView) view.findViewById(R.id.winboll_webview);
}
/**
* 绑定点击事件Java 7 匿名内部类无Lambda
*/
private void initEvents() {
// 功能按钮点击事件
mBtnLoad.setOnClickListener(this);
mBtnRefresh.setOnClickListener(this);
mBtnStop.setOnClickListener(this);
mBtnForward.setOnClickListener(this);
mBtnBack.setOnClickListener(this);
// 输入框软键盘“前往”按钮事件Java 7 匿名内部类实现)
mEtUrl.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, android.view.KeyEvent event) {
// 处理软键盘“前往”点击
loadUrlFromInput();
return true;
}
});
}
/**
* 初始化WinBoLLViewJava 7 显式调用,无方法引用)
*/
private void initWinBoLLView() {
// 绑定进度条
mWinBoLLView.setProgressBar(mProgressBar);
// 设置页面状态监听this 实现 OnPageStatusListener
mWinBoLLView.setOnPageStatusListener(this);
// 预加载默认页面兼容Java 7 字符串拼接)
//String defaultUrl = "https://www.baidu.com";
String defaultUrl = "https://www.winboll.cc";
mWinBoLLView.loadUrlSafe(defaultUrl);
mEtUrl.setText(defaultUrl);
}
/**
* 点击事件处理Java 7 switch-case 语句无增强switch
*/
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.btn_load) {
// 加载输入框中的URL
loadUrlFromInput();
} else if (id == R.id.btn_refresh) {
// 刷新当前页面
mWinBoLLView.refreshPage();
} else if (id == R.id.btn_stop) {
// 停止页面加载
mWinBoLLView.stopPageLoad();
} else if (id == R.id.btn_forward) {
// 前进(无历史则提示)
if (!mWinBoLLView.goForwardSafe()) {
showToast("无前进历史");
}
} else if (id == R.id.btn_back) {
// 后退(无历史则提示)
if (!mWinBoLLView.goBackSafe()) {
showToast("无后退历史");
}
}
}
/**
* 从输入框获取URL并加载Java 7 显式空值校验)
*/
private void loadUrlFromInput() {
// 空值校验Java 7 显式判断无Objects.requireNonNull
if (mEtUrl == null) {
showToast("控件初始化失败");
return;
}
String url = mEtUrl.getText().toString().trim();
// 调用WinBoLLView安全加载方法
mWinBoLLView.loadUrlSafe(url);
// 隐藏软键盘
hideSoftKeyboard();
}
/**
* 隐藏软键盘Java 7 显式获取系统服务无Lambda简化
*/
private void hideSoftKeyboard() {
if (getActivity() == null) {
return;
}
// 获取InputMethodManagerJava 7 显式强转)
InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
if (imm != null && getActivity().getCurrentFocus() != null) {
imm.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), 0);
}
}
/**
* 显示Toast提示Java 7 简化封装,避免重复代码)
*/
private void showToast(String msg) {
if (getActivity() == null || msg == null) {
return;
}
// Java 7 显式创建Toast无Toast.makeText简化链式调用
Toast toast = Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT);
toast.show();
}
// ------------------- WinBoLLView.OnPageStatusListener 实现Java 7 显式重写) -------------------
@Override
public void onPageStarted(String url) {
// 页面开始加载更新输入框URLJava 7 显式非空判断)
if (mEtUrl != null && url != null) {
mEtUrl.setText(url);
}
}
@Override
public void onPageFinished(String url) {
// 页面加载完成更新输入框URL
if (mEtUrl != null && url != null) {
mEtUrl.setText(url);
}
}
@Override
public void onPageError(String errorMsg) {
// 页面加载错误:显示错误提示
showToast("加载失败:" + errorMsg);
}
// ------------------- 生命周期管理防止内存泄漏Java 7 显式重写) -------------------
@Override
public void onDestroyView() {
super.onDestroyView();
// 销毁WinBoLLView释放资源避免内存泄漏
if (mWinBoLLView != null) {
mWinBoLLView.destroyWebView();
mWinBoLLView = null;
}
// 置空控件帮助GC回收
mEtUrl = null;
mBtnLoad = null;
mBtnRefresh = null;
mBtnStop = null;
mBtnForward = null;
mBtnBack = null;
mProgressBar = null;
}
@Override
public void onDestroy() {
super.onDestroy();
// 彻底释放资源
if (mWinBoLLView != null) {
mWinBoLLView.destroyWebView();
mWinBoLLView = null;
}
}
}

View File

@@ -0,0 +1,232 @@
package cc.winboll.studio.winboll.views;
import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/27 11:05
* @Describe 自定义WebView控件WinBoLLView
* 集成进度监听、页面加载控制、安全配置等核心能力
*/
public class WinBoLLView extends WebView {
public static final String TAG = "WinBoLLView";
private ProgressBar mProgressBar; // 页面加载进度条(可选,增强用户体验)
private OnPageStatusListener mStatusListener; // 页面状态监听回调
private boolean mIsLoading = false; // 自定义加载状态标记替代系统isLoading()
// 构造方法兼容代码创建和XML布局引用
public WinBoLLView(Context context) {
super(context);
initWebViewSettings();
initWebViewClient();
}
public WinBoLLView(Context context, AttributeSet attrs) {
super(context, attrs);
initWebViewSettings();
initWebViewClient();
}
public WinBoLLView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initWebViewSettings();
initWebViewClient();
}
/**
* 初始化WebView基础配置安全+性能+兼容性)
*/
private void initWebViewSettings() {
WebSettings settings = getSettings();
// 基础功能配置
settings.setJavaScriptEnabled(true); // 启用JS根据需求决定是否开启
settings.setSupportZoom(true); // 支持缩放
settings.setBuiltInZoomControls(true); // 显示缩放控件
settings.setDisplayZoomControls(false); // 隐藏系统缩放控件优化UI
settings.setLoadWithOverviewMode(true); // 自适应屏幕
settings.setUseWideViewPort(true); // 支持宽视角
// 缓存配置(提升加载速度)
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setDomStorageEnabled(true); // 启用DOM存储
settings.setDatabaseEnabled(true); // 启用数据库存储
// 安全配置防止XSS和恶意跳转
settings.setJavaScriptCanOpenWindowsAutomatically(false); // 禁止JS自动打开窗口
setBackgroundColor(0x00000000); // 透明背景(避免白屏闪烁)
// 适配HTTPS和HTTP混合内容Android 5.0+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
}
}
/**
* 初始化WebViewClient和ChromeClient控制页面加载和进度
*/
private void initWebViewClient() {
// 控制页面跳转(不打开系统浏览器)
setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// 拦截URL加载使用当前WebView打开
view.loadUrl(request.getUrl().toString());
return true;
}
@Override
public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// 页面开始加载:更新自定义加载状态标记
mIsLoading = true;
// 更新进度条+回调状态
if (mProgressBar != null) mProgressBar.setVisibility(VISIBLE);
if (mStatusListener != null) mStatusListener.onPageStarted(url);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载完成:更新自定义加载状态标记
mIsLoading = false;
// 隐藏进度条+回调状态
if (mProgressBar != null) mProgressBar.setVisibility(GONE);
if (mStatusListener != null) mStatusListener.onPageFinished(url);
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
// 页面加载错误:更新自定义加载状态标记
mIsLoading = false;
// 回调错误信息
if (mStatusListener != null) {
String errorMsg = error.getDescription().toString();
mStatusListener.onPageError(errorMsg);
}
}
});
// 监听页面加载进度
setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
// 更新进度条进度
if (mProgressBar != null) mProgressBar.setProgress(newProgress);
}
});
}
// ------------------- 对外暴露的控制方法供Fragment调用 -------------------
/**
* 加载URL增加空值校验+优先HTTPS协议
*/
public void loadUrlSafe(String url) {
if (url == null || url.trim().isEmpty()) {
if (mStatusListener != null) mStatusListener.onPageError("URL不能为空");
return;
}
// 协议补全优先HTTPS兼容HTTP解决ERR_CLEARTEXT_NOT_PERMITTED
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url; // 改为优先HTTPS而非HTTP
}
super.loadUrl(url);
}
/**
* 刷新当前页面使用自定义mIsLoading判断加载状态
*/
public void refreshPage() {
if (mIsLoading) { // 替换原isLoading(),使用自定义标记
stopLoading(); // 若正在加载,先停止再刷新
}
reload();
}
/**
* 停止页面加载使用自定义mIsLoading判断加载状态
*/
public void stopPageLoad() {
if (mIsLoading) { // 替换原isLoading(),使用自定义标记
stopLoading();
mIsLoading = false; // 停止后更新状态标记
}
}
/**
* 前进(判断是否有前进历史)
*/
public boolean goForwardSafe() {
if (canGoForward()) {
goForward();
return true;
}
return false;
}
/**
* 后退(判断是否有后退历史)
*/
public boolean goBackSafe() {
if (canGoBack()) {
goBack();
return true;
}
return false;
}
// ------------------- 辅助功能:进度条和状态监听 -------------------
/**
* 设置进度条绑定Fragment中的进度条控件
*/
public void setProgressBar(ProgressBar progressBar) {
this.mProgressBar = progressBar;
if (mProgressBar != null) {
mProgressBar.setMax(100);
mProgressBar.setVisibility(GONE);
}
}
/**
* 设置页面状态监听供Fragment接收加载状态
*/
public void setOnPageStatusListener(OnPageStatusListener listener) {
this.mStatusListener = listener;
}
/**
* 页面状态监听接口
*/
public interface OnPageStatusListener {
void onPageStarted(String url); // 页面开始加载
void onPageFinished(String url); // 页面加载完成
void onPageError(String errorMsg); // 页面加载错误
}
// ------------------- 资源释放(防止内存泄漏) -------------------
/**
* 销毁WebView必须在Fragment销毁时调用
*/
public void destroyWebView() {
// 停止加载并清空历史
stopLoading();
mIsLoading = false; // 销毁时重置加载状态
clearHistory();
// 移除所有WebViewClient避免内存泄漏
setWebViewClient(null);
setWebChromeClient(null);
// 销毁WebView
destroy();
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<corners android:radius="8dp"/>
<stroke android:color="@android:color/darker_gray" android:width="1dp"/>
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<corners android:radius="24dp"/>
<stroke android:color="@android:color/darker_gray" android:width="1dp"/>
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@android:color/darker_gray"/>
<item android:id="@android:id/progress">
<clip android:drawable="@android:color/holo_blue_light"/>
</item>
</layer-list>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 顶部地址栏区域(水平布局:输入框+功能按钮) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@drawable/bg_browser_top"
android:padding="4dp">
<!-- 地址输入框 -->
<EditText
android:id="@+id/et_url"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="请输入URL或搜索内容"
android:paddingHorizontal="12dp"
android:background="@drawable/bg_edittext"
android:singleLine="true"
android:imeOptions="actionGo"
android:inputType="textUri" /> <!-- 提示输入URL格式 -->
<!-- 功能按钮区域(水平排列:加载、刷新、停止、前进、后退) -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="4dp">
<Button
android:id="@+id/btn_load"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="加载"
android:textSize="12sp"
android:layout_marginHorizontal="2dp"/>
<Button
android:id="@+id/btn_refresh"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="刷新"
android:textSize="12sp"
android:layout_marginHorizontal="2dp"/>
<Button
android:id="@+id/btn_stop"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="停止"
android:textSize="12sp"
android:layout_marginHorizontal="2dp"/>
<Button
android:id="@+id/btn_forward"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="前进"
android:textSize="12sp"
android:layout_marginHorizontal="2dp"/>
<Button
android:id="@+id/btn_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="后退"
android:textSize="12sp"
android:layout_marginHorizontal="2dp"/>
</LinearLayout>
</LinearLayout>
<!-- 页面加载进度条位于地址栏下方WebView上方 -->
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
android:progressDrawable="@drawable/progress_bar_style"
android:visibility="gone"/>
<!-- 自定义WebViewWinBoLLView -->
<cc.winboll.studio.winboll.views.WinBoLLView
android:id="@+id/winboll_webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 允许访问 winboll.cc 及其子域名(原配置 -->
<!-- 允许特定域名的HTTP明文请求如winboll.com -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">winboll.cc</domain>
</domain-config>
<!-- **新增:允许访问 IP 地址 10.8.0.250** -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.8.0.250</domain> <!-- 不包含子域名 -->
<domain includeSubdomains="true">winboll.cc</domain> <!-- 你的目标域名 -->
<domain includeSubdomains="true">www.winboll.cc</domain> <!-- 其他需要兼容的域名 -->
</domain-config>
<!-- 默认允许HTTPS全局生效 -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>