feat: 新增ProtectModeTextView自定义控件

1. 继承LinearLayout,内置TextView与0~12刻度SeekBar
2. 刻度0保持原始文本不打乱,1~12为每组相邻字符个数
3. 按刻度固定长度从头至尾字符分组,分组列表随机打乱后拼接输出
4. 支持含空格/标点完整字符解析,对外提供setContentText设置文本接口
This commit is contained in:
2026-05-08 19:35:39 +08:00
parent 3231cd557a
commit 3b60a3b713
5 changed files with 211 additions and 35 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Wed Feb 11 05:29:19 HKT 2026 #Fri May 08 11:33:06 GMT 2026
stageCount=8 stageCount=8
libraryProject= libraryProject=
baseVersion=15.12 baseVersion=15.12
publishVersion=15.12.7 publishVersion=15.12.7
buildCount=0 buildCount=9
baseBetaVersion=15.12.8 baseBetaVersion=15.12.8

View File

@@ -11,20 +11,29 @@ import android.view.View;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.LogView; import cc.winboll.studio.libappbase.LogView;
import cc.winboll.studio.mymessagemanager.R; import cc.winboll.studio.mymessagemanager.R;
import cc.winboll.studio.mymessagemanager.views.ProtectModeTextView;
public class UnitTestActivity extends Activity { public class UnitTestActivity extends Activity {
public static final String TAG = "UnitTestActivity"; public static final String TAG = "UnitTestActivity";
LogView mLogView; LogView mLogView;
// 新增自定义控件
ProtectModeTextView mProtectModeTv;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_unittest); setContentView(R.layout.activity_unittest);
mLogView = findViewById(R.id.logview); mLogView = findViewById(R.id.logview);
mLogView.start(); mLogView.start();
// 初始化ProtectModeTextView
mProtectModeTv = findViewById(R.id.protect_mode_tv);
// 设置测试文本,可自行修改
String testText = "abcdefghijklmnopqrstuvwxyz消息管理 隐私保护 文本随机组合 滑动刻度测试1234567890";
mProtectModeTv.setContentText(testText);
} }
public void onMain(View view) { public void onMain(View view) {
@@ -34,3 +43,4 @@ public class UnitTestActivity extends Activity {
AddressUtils_Test.main(this); AddressUtils_Test.main(this);
} }
} }

View File

@@ -0,0 +1,136 @@
package cc.winboll.studio.mymessagemanager.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import cc.winboll.studio.mymessagemanager.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
/**
* 保护模式自定义控件
* 最终规则:
* 1. 刻度范围 0~12
* 2. 刻度值 = 每一组截取【相邻字符个数】
* 3. 从头到尾按固定长度切块分组
* 4. 所有分组收集后随机打乱再拼接输出
* 5. 刻度0 = 不打乱,显示原文
* 6. 按原生字符计算(包含空格、标点)
*/
public class ProtectModeTextView extends LinearLayout {
private TextView tvContent;
private SeekBar seekBarScale;
private String originText;
private List<Character> charAllList;
private final Random random = new Random();
public ProtectModeTextView(Context context) {
super(context);
initView(context);
}
public ProtectModeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public ProtectModeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.layout_protect_mode_textview, this, true);
tvContent = findViewById(R.id.tv_content);
seekBarScale = findViewById(R.id.seek_bar_scale);
// 刻度 0 ~ 12
seekBarScale.setMax(12);
seekBarScale.setProgress(0);
charAllList = new ArrayList<>();
seekBarScale.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
handleTextLogic(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
}
public void setContentText(String text) {
this.originText = text;
convertToCharList(text);
handleTextLogic(seekBarScale.getProgress());
}
private void convertToCharList(String text) {
charAllList.clear();
if (text == null || text.isEmpty()) {
return;
}
char[] chars = text.toCharArray();
for (char c : chars) {
charAllList.add(c);
}
}
private void handleTextLogic(int groupSize) {
if (charAllList.isEmpty()) {
tvContent.setText(originText);
return;
}
// 刻度0 原样不打乱
if (groupSize <= 0) {
tvContent.setText(originText);
return;
}
List<String> groupList = new ArrayList<>();
int totalLen = charAllList.size();
// 从头到尾 按 groupSize 个相邻字符切块
int index = 0;
while (index < totalLen) {
StringBuilder sb = new StringBuilder();
// 每一组取 groupSize 个相邻字符
for (int i = 0; i < groupSize && index < totalLen; i++) {
sb.append(charAllList.get(index));
index++;
}
groupList.add(sb.toString());
}
// 所有分组随机打乱
Collections.shuffle(groupList, random);
// 拼接输出
StringBuilder result = new StringBuilder();
for (String item : groupList) {
result.append(item);
}
tvContent.setText(result.toString());
}
public String getOriginText() {
return originText;
}
public void resetSeekBar() {
seekBarScale.setProgress(0);
}
}

View File

@@ -1,44 +1,52 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="500dp"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:padding="10dp">
<LinearLayout <!-- 新增自定义ProtectModeTextView -->
android:orientation="horizontal" <cc.winboll.studio.mymessagemanager.views.ProtectModeTextView
android:layout_width="match_parent" android:id="@+id/protect_mode_tv"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:gravity="right"> android:layout_height="wrap_content"
android:layout_marginBottom="15dp"/>
<Button <LinearLayout
android:layout_width="wrap_content" android:orientation="horizontal"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="Test Main" android:layout_height="wrap_content"
android:onClick="onMain" android:gravity="right">
android:textAllCaps="false"/>
</LinearLayout> <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Test Main"
android:onClick="onMain"
android:textAllCaps="false"/>
</LinearLayout> </LinearLayout>
</ScrollView> </LinearLayout>
<cc.winboll.studio.libappbase.LogView </ScrollView>
android:layout_width="match_parent"
android:layout_height="0dp" <cc.winboll.studio.libappbase.LogView
android:layout_weight="1.0" android:layout_width="match_parent"
android:id="@+id/logview"/> android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/logview"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,22 @@
<?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="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:gravity="center"/>
<SeekBar
android:id="@+id/seek_bar_scale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"/>
</LinearLayout>