android的listview加入EditText

在日常开发中,ListView 是我们常用的控件,也是遇到坑比较多的一个控件。在之前的项目中,有这样的一个布局需求,在 ListView 的 item 中包含有 EditText,第一个问题就是焦点问题,会发现 edittext 获取不到焦点。

1.焦点问题
比如我们有如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>

####MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {
ListView mListView;
MyAdapter mMyAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listView);
mMyAdapter = new MyAdapter(this);
mListView.setAdapter(mMyAdapter);
}}

####MyAdapter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyAdapter extends BaseAdapter {
private Context mContext;
public MyAdapter(Context context) {
this.mContext = context;
}
@Override
public int getCount() {
return 20;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
editText = new EditText(mContext);
convertView = editText;
} else {
editText = (EditText) convertView;
}
System.out.println("current pos:" + position);
return convertView;
}}

当你运行上述简单的代码后发现 EditText 是无法获取焦点的,导致无法输入任何东东,那么原因何在呢?

####其实,是 listview 先于子 item 抢占了焦点,那么我们首先想到的就是让 listview 失去焦点,让子 item 获取焦点(当然,listview 的 onitem 相关监听事件会失效)。
mListView.setFocusable(false);

这是再运行发现键盘弹出了,可是 editText 获取到焦点然后又失去了,需要你手动再次点击才能获取到,然后才能输入。
而且当你输入完毕,关闭软键盘,发现输入的东西不见了,自动清空。这又产生了两个问题。

第一个问题是 listview 每次调用 getview 都会使 EditText 失去焦点,第二个问题归结于下面要讲的 listview 的 item 复用产生的问题。

第一种方式行不通,查询相关资料发现,可以通过给 listview 的 item 的根布局设置 descendantFocusability 属性。

1
2
3
4
#####android:descendantFocusability 属性有三个值:
beforeDescendants:viewgroup 会优先其子类控件而获取到焦点
afterDescendants:viewgroup 只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup 会覆盖子类控件而直接获得焦点

####那么我们修改 adapter 中的 getView 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EditText editText;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);
editText = (EditText) convertView.findViewById(R.id.editText);
convertView.setTag(editText);
} else {
editText = (EditText) convertView.getTag();
}
System.out.println("current pos:" + position);
return convertView;
}

####list_edittext.xml

1
2
3
4
5
6
7
8
9
10
11
<?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:descendantFocusability="beforeDescendants"
android:orientation="vertical">
<EditText android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

发现还是无效果,其实我们少了一句关键的代码,就是给相应的 activity 设置 windowSoftInputMode= adjustPan 即可。。

##终上所述,我认为的解决方案就是给 ListView 或者 ListView 的 item 的根布局添加 android:descendantFocusability="beforeDescendants",然后设置相应的 activity 的 windowSoftInputMode 属性为 adjustPan 。

##2.数据问题
解决完焦点问题后,另一个问题就是 edittext 的数据问题了。当我们在当前屏幕的 edittext 中输入东东后,往下滑,发现下面的 edittext 自动输入了我们输入过得东东,这明显是我们不愿意看到的。

其实这是由于 getView 方法的复用 view 导致的,加入你在 position=0 的 edittext 中输入了内容,当你往下滑时,当 position 为 0 的 view 完全消失时,该 view 会被加入到 mActiveViews[]中,当下方的 item 检测到由可用的 view,则从该数组中取出,所以下方的 edittext 的内容会跟上面你输入的一样,其实就是同一个 edittext。关于 listview 源码级解析详见链接

#####解决方案——保存 edittext 的内容

修改 adapter 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//新增一个数组用于保存 edittext 的内容
private SparseArray<String> mStringSparseArray;

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
EditTextHolder editTextHolder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_edittext, parent, false);

editTextHolder = new EditTextHolder();
editTextHolder.mEditText = (EditText) convertView.findViewById(R.id.editText);

editTextHolder.mMyTextWatcher = new MyTextWatcher(position, mStringSparseArray);
//给edittext设置watcher
editTextHolder.mEditText.addTextChangedListener(editTextHolder.mMyTextWatcher);

convertView.setTag(editTextHolder);

} else {
editTextHolder = (EditTextHolder) convertView.getTag();
//由于复用了 edittext,导致他的 watcher 里的 position 还是之前的 positiono,所以需要通知
//watcher 更新 positon,才能保存正确的 positon 的值
editTextHolder.updatePosition(position);
}
System.out.println(position);
editTextHolder.mEditText.setText(mStringSparseArray.get(position));
return convertView;
}
static class EditTextHolder {
EditText mEditText;
MyTextWatcher mMyTextWatcher;
public void updatePosition(int position) {
mMyTextWatcher.updatePosition(position);
}
}

static class MyTextWatcher implements TextWatcher {
private int position;
private SparseArray<String> sparseArray;
//更新 postion
public void updatePosition(int position) {
this.position = position;
}
public MyTextWatcher(int position, SparseArray<String> sparseArray) {
this.position = position;
this.sparseArray = sparseArray;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) { }
@Override
public void afterTextChanged(Editable s) {
//保存 edittext 的值
sparseArray.put(position, s.toString());
}
}

运行代码,发现 edittext 数据错乱问题解决,此方法同样适用于 checkbox 错乱等问题。

android的listview解决事件穿透问题

bug 重现:通过 mListView.setOnItemClickListener 设置 item 点击事件,正常情况下没有问题,但当 item 里面嵌套了抢焦点的控件(比如 Button ,CheckBox 等),那么点击 item 的时候,Button 等抢焦点的控件会抢先反应,就会导致点击 item 时没有反应。

解决办法:想要 item 有自己的焦点,Button 等控件有自己的焦点的话,需要在 item 的根控件里面设置 android:descendantFocusability="blocksDescendants",这个属性值表示子有子的焦点,父有父的焦点。

java 代码

1
2
3
4
5
6
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Log.i(TAG, "onItemClick: ------");
}
});

item 布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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:descendantFocusability="blocksDescendants"
android:orientation="horizontal">

<!--标题-->
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text="标题" />

<!--删除按钮-->
<Button
android:id="@+id/bt_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"/>

</LinearLayout>

属性 android:descendantFocusability 的值有三种:

1
2
3
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点,也就是各有各的焦点

Mac下,Beyond Compare软件破解

一、原理
Beyond Compare 每次启动后会先检查注册信息,试用期到期后就不能继续使用。解决方法是在启动前,先删除注册信息,然后再启动,这样就可以永久免费试用了。

二、下载
首先下载 Beyond Compare 最新版本,链接如下:https://www.scootersoftware.com/download.php

三、安装
下载完成后,直接安装。

四、创建 BCompare 文件

  1. 进入 Mac 应用程序目录下,找到刚刚安装好的 Beyond Compare,路径如下:/Applications/Beyond Compare.app/Contents/MacOS
  2. 修改启动程序文件 BCompare 为 BCompare.real
  3. 在当前目录下新建一个文件 BCompare,文件内容如下:
1
2
3
#!/bin/bash
rm "/Users/$(whoami)/Library/Application Support/Beyond Compare/registry.dat"
"`dirname "$0"`"/BCompare.real $@
  1. 保存 BCompare 文件。
  2. 修改文件的权限:
1
chmod a+x /Applications/Beyond\ Compare.app/Contents/MacOS/BCompare

以上步骤完成后,再次打开 Beyond Compare 就可以正常使用了。

Android透明度百分比转十六进制表

100% — FF
99% — FC
98% — FA
97% — F7
96% — F5
95% — F2
94% — F0
93% — ED
92% — EB
91% — E8

90% — E6
89% — E3
88% — E0
87% — DE
86% — DB
85% — D9
84% — D6
83% — D4
82% — D1
81% — CF

80% — CC
79% — C9
78% — C7
77% — C4
76% — C2
75% — BF
74% — BD
73% — BA
72% — B8
71% — B5

70% — B3
69% — B0
68% — AD
67% — AB
66% — A8
65% — A6
64% — A3
63% — A1
62% — 9E
61% — 9C

60% — 99
59% — 96
58% — 94
57% — 91
56% — 8F
55% — 8C
54% — 8A
53% — 87
52% — 85
51% — 82

50% — 80
49% — 7D
48% — 7A
47% — 78
46% — 75
45% — 73
44% — 70
43% — 6E
42% — 6B
41% — 69

40% — 66
39% — 63
38% — 61
37% — 5E
36% — 5C
35% — 59
34% — 57
33% — 54
32% — 52
31% — 4F

30% — 4D
29% — 4A
28% — 47
27% — 45
26% — 42
25% — 40
24% — 3D
23% — 3B
22% — 38
21% — 36

20% — 33
19% — 30
18% — 2E
17% — 2B
16% — 29
15% — 26
14% — 24
13% — 21
12% — 1F
11% — 1C

10% — 1A
9% — 17
8% — 14
7% — 12
6% — 0F
5% — 0D
4% — 0A
3% — 08
2% — 05
1% — 03
0% — 00

jar包反编译

编译 java 代码 Jar 包

  1. jd-gui (不能反编译 groovy)
  2. jdec (在线反编译)

编译 groovy 代码 Jar 包

  1. jdec (在线反编译)

html获取可见窗口大小

为了获取网页可见区域的高度,不同终端滚动元素指向 html 中 dom 是不一样的。
PC 端:document.scrollingElement = document.documentElement
Mobile 端:document.scrollingElement = document.body

兼容性

浏览器窗口视图

屏幕大小

1
2
3
4
5
6
window.screen.height:屏幕分辨率的高(屏幕的高度 )
window.screen.width:屏幕分辨率的宽 (屏幕的宽度 )
window.screen.availHeight:屏幕可用工作区的高
window.screen.availWidth:屏幕可用工作区的高
window.screenTop:浏览器窗口距离电脑屏幕上边界的距离
window.screenLeft:浏览器窗口距离电脑屏幕左边界的距离

window 大小

1
2
3
4
window. innerWidth :浏览器可视区域的内宽度,包含滚动条
window.innerHeight:浏览器可视区域的内高度,包含滚动条
window.outerWidth:浏览器宽度
window.outerHeight:浏览器高度

滚动偏移量

1
2
3
4
5
6
window.pageYOffset:需要网页存在滚动条才可以,存在竖向滚动条时,网页正文向下滚动一段距离n px,该值即为n
window.pageXOffset:同理,无滚动条值为0
document.body.scrollTop:网页下滑的距离(与上一对值相同,滚动条位置)
document.body.scrollLeft:网页左滑的距离
document.documentElement.scrollTop:与上一对值相同(与上一对都是获得滚动条位置,但是存在兼容问题)
document.documentElement.scrollLeft:兼容处理方案

兼容问题,具体与 html 文件头的<DOCTYPE…>有关
var srollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;

可见区域大小

1
2
3
4
document.body.clientWidth:获取可视区域的宽(可以进行页面展示的,不包含边线,例如body{border:10px solid red;})
documment.body.clientHeight:获取可视区域的高(可以进行页面展示的)
document.documentElement.clientWidth:页面可视宽度,但是不包含滚动条组件的十几px像素
document.documentElement.clientHeight:可视区域高度, 实际上就是 元素,只不过显示的是可见的部分,即浏览器窗口大小(网页无滚动条时与window.innerHeight同值)

滚动元素大小

1
2
3
4
document.documentElement.scrollWidth:获取网页正文全文宽(包括滚动部分)
document.documentElement.scrollHeight:获取网页正文全文高(包括滚动部分)
document.body.scrollWidth:与上一对值相同
document.body.scrollHeight:与上一对值相同

flutter常见问题

  1. 新版 flutter,ancestorWidgetOfExactType 方法过时
    老方法:
1
2
3
4
5
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}

新方法:

1
2
3
4
static T of<T extends BlocBase>(BuildContext context) {
BlocProvider<T> provider = context.findAncestorWidgetOfExactType<BlocProvider<T>>();
return provider.bloc;
}

android常见问题汇总

Android8.0/8.1 屏幕旋转崩溃

原因:

android:screenOrientation=”portrait”
activity透明<item name="android:windowIsTranslucent">true</item>

解决方式:
去掉启动页的透明风格属性,并且启动页 style 加上如下属性:

1
<item name="android:windowDisablePreview">true</item>

禁止屏幕旋转

方法一,设置activity的

1
2
screenOrientation=“portrait”

方法二,activity的onCreate中

1
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // 禁用横屏

被后台杀死后,Android应用如何重新走闪屏逻辑

Android应用运行在后台的时候,经常被系统的LowMemoryKiller杀掉,当用户再次点击icon或者从最近的任务列表启动的时候,进程会被重建,并且恢复被杀之前的现场。什么意思呢?假如APP在被杀之前的Activity堆栈是这样的,A<B<C,C位于最上层

后台杀死与恢复的堆栈.jpg

APP被后台杀死后,APP端进程被销毁了,也就不存在什么Activity了,也就没有什么Activity堆栈,不过AMS的却是被保留了下来:

后台杀死与恢复的堆栈-杀后.jpg

当用户再次启动APP时候会怎么样呢?这个时候,首先看到其实C,而不是栈底部的A,也就是说往往被杀死后,恢复看到的第一个界面是用户最后见到的那个界面。

后台杀死与恢复的堆栈-恢复.jpg

而用户点击返回,看到的就是上一个界面B,其次是A

后台杀死与恢复的堆栈-恢复b.jpg

之所以这样是因为APP端Activity的创建其实都是由AMS管理的,AMS严格维护这APP端Activity对应的ActivityRecord栈,可以看做当前APP的场景,不过,APP端Activity的销毁同AMS端ActivityRecord的销毁并不一定是同步的,最明显的就是后台杀死这种场景。Android为了能够让用户无感知后台杀死,就做了这种恢复逻辑,不过,在开发中,这种逻辑带了的问题确实多种多样,甚至有些产品就不希望走恢复流程,本文就说说如何避免走恢复流程。结合常见的开发场景,这里分为两种,一种是针对推送唤起APP,一种是针对从最近任务列表唤起APP(或者icon)。

从最近的任务列表唤起,不走恢复流程

首先,APP端必须知道当前Activity的启动是不是在走恢复流程,Activity有一个onCreate方法,在ActivityThread新建Activity之后,会回调该函数,如果是从后台杀死恢复来的,回调onCreate的时候会传递一个非空的Bundle savedInstanceState给当前Activity,只要判断这个非空就能知道是否是恢复流程。

1
2
3
4
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

知道恢复流程之后,如何处理呢?其实很简单,直接吊起闪屏页就可以了,不过这里有一点要注意的是,在启动闪屏页面的时候,必须要设置其IntentFlag:Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK,这样做的理由是为了清理之前的场景,不然之前的ActivityRecord栈仍然保留在ActivityManagerService中,具体实现如下,放在BaseActivity中就可以:

1
2
3
Intent intent = new Intent(this, SplashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

如果不设置会怎么样呢?举个例子,最常见的就是闪屏之后跳转主界面,主界面经常有router逻辑,并且其启动模式一般都是singleTask,处理一些推送,所以其onCreate跟onNewIntent都有相应的处理,如果不设置,在闪屏结束后,在startActivity启动主界面的时候,其实是先走恢复逻辑,然后走singleTask的onNewIntent逻辑,也就是说,onNewIntent跟onCreate是会同时调用的,也可能就会引发重复处理的逻辑,因此最好清理干净。

从推送唤起被杀APP时,如何走闪屏逻辑

对于推送消息的处理,其路由器一般放在MainActivity,并且在onCreate跟onNewIntent都有添加,如果APP存活的情况,可以直接跳转目标页面,如果APP被杀,这个时候,希望先跳转主界面,再跳转目标页面,在效果上来看就是,用户先看到目标页面,点击返回的时候再看到主界面,如果加上闪屏,希望达到的效果是先看到闪屏、点击返回看到目标页,再点击返回看到主页面。如果简单划分一下推送场景,可以看做一下三种

  • 进程存活,Activity存活
  • 进程存活,但是没有Activity存活
  • 进程不存在(无论是否被杀)

其实后面两种完全可以看做一种,这个时候,都是要先start MainActivity,然后让MainActivity在其OnCreate中通过startActivityForResult启动SplashActivity,SplashActivity返回后,在start TargetActivity。下面的讨论都是针对后面两种,需要做的有两件事

  • 一是:检测出后面两种场景,并且在唤起主界面的时候需要添加Intent.FLAG_ACTIVITY_CLEAR_TASK清理之前的现场
  • 二是:在MainActivity的路由系统中,针对这两种场景要,先跳转闪屏,闪屏回来后,再跳转推送页
    如何判断呢,后面两种场景其实只需要判断是否有Activity存活即可,也就是查查APP的topActivity是否为null,注意不要去向AMS查询,而是在本地进程中查询,可以通过反射查询ActivityThread的mActivities,也可以根据自己维护的Activity堆栈来判断,判断没有存活Activity的前提下,就跳转主页面去路由
1
2
3
4
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setDate(跳转的Uri scheme)
startActivity(intent);

在MainActivity的路由中,需要准确区分是否是推送跳转进来的,如果不是推送跳转进来,就不需要什么特殊处理,如果是推送跳转进来一定会携带跳转scheme数据,根据是否携带数据做区分即可,看一下MainActivity的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri= getIntent().getData();
// 只有在intent被设置了跳转数据的时候才去跳转,一般是推送就来,如果冷启动,是没有数据的
if(uri!=null){
SplashActivity.startActivityForResult(this,JUMP_TO_TARGET)
}
}
//Intent.FLAG_ACTIVITY_CLEAR_TASK保证了onNewIntent被调用的时候,进程一定是正常活着的
@Override
protected void onNewIntent(Intent intent) {
Uri uri= intent.getData();
intent.setData(null);
router(uri);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==JUMP_TO_TARGET && requestCode == RESULT_OK){
router(getIntent().getData());
getIntent().setData(null);
}
}

private void router(Uri uri) {

}

通过上面两部分的处理,基本能够满足APP“死亡”的情况下,先跳转闪屏的需求。