Android 使用 RecyclerView 分隔线问题

自从Google推出了RecyclerView作为ListView功能更加强大的替代品之后,越来越多的APP开始使用这个控件。RecyclerView可以优化程序性能,并且由于功能上的高度解耦,可以无成本地在ListView、GridView以及瀑布流之间切换,但是也正因为如此,原来在ListView中理所当然的东西在RecyclerView中要自己来实现,比如分隔线,比如OnItemClickListener等(不是特别理解为什么Listener还要我们自己实现,我的做法就是往Adapter里扔回调)

说到分隔线,可以去实现RecyclerView.ItemDecoration来定制,在鸿洋大大的博客里有非常详细的讲解,按照这个来做其实也不麻烦,而且一次编写复用终生,但是我做的时候还是想偷懒,不想去写那么多代码,所以想用背景图片来实现分隔线效果。

我的RecyclerView中每个item的背景是这样的,用了selector实现点击前后的变色效果

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/list_item_bg_pressed" android:state_pressed="true"/>
<item android:drawable="@drawable/list_item_bg_unpressed" android:state_pressed="false"/>
</selector>

其中@drawable/list_item_bg_pressed是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 第一层,露出个小边当分隔线 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/color_list_item_divider"/>
<padding android:bottom="@dimen/dimen_list_item_divider_height"/>
</shape>
</item>
<!-- 第二层,真正的背景色 -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/color_list_item_bg_pressed"/>
</shape>
</item>
</layer-list>

使用layer-list来实现效果:第一层使用分割线的颜色,并在底部padding一个窄边,第二层使用正常背景颜色,这样写而不是直接用一张图片代替的原因是:如果使用普通图片作为背景,当图片被拉伸时分隔线会明显变粗,效果非常不美观(当然.9图片没有问题,想了想还是直接用代码写了),而用代码就没有这个问题。这种效果我还用来实现一些输入框。

@drawable/list_item_bg_unpressed 同理,就是把第二层的颜色换了一下。

那么为什么不直接用一张图片来代替呢?因为有些情况下RecyclerView每个条目的高度并不是一样的,这样对于普通图片会产生纵向拉伸的效果,使得分隔线变得非常可怕,而用代码来实现的.9效果则不会有这个问题。

javax.el.PropertyNotFoundException: Property 'XXX' not readable

使用spring框架,自己组装了一个信息搭载类扔到JSP中渲染,大致是这样:

1
UserBasicInfo userInfo = new UserBasicInfo(user);

以及

1
<img id="user_avatar" src="${userInfo.avatar}" class="img-responsive col-md-6" alt="用户头像"/>

这时候运行时就会出现标题中的错误,UserBasicInfo是pojo,是放在UserController.java中的一个非public类。试过把他做成bean仍会有这个错误,之后考虑spring是使用代理来做这些事情的,默认访问权限的类spring可能访问不到,就把这个类单独做成一个文件做成public的,问题解决。

Hibernate com.mysql.jdbc.exceptions.MySQLSyntaxErrorException MYSQL

大作业,使用SpringMVC + Hibernate搭一个小网站,但是中途出现了这个问题,困扰了我一天时间。

是的这个问题很烦人,所以如果你已经尝试过:

  • 查找StackOverflow
  • 多次更换数据库方言
  • 更换数据库驱动
  • 你甚至丧心病狂地更换数据库

仍然没有结果的话,那么你的问题应该就是

你丫是不是把数据库保留字当列名了,真以为SQL不是编程语言啊

血泪的教训,以上

关于程序员的一点感想

对于一个作家,最重要的是能写出出色的文章,文章的水平如何,和作家用什么颜色、形状、粗细的笔来写没有任何关系。假如出现这么一幕场景:许多作家围坐在桌旁,口水飞溅地争论:

“英雄才是最好的笔!”

“瞎说!关勒铭才是最好的笔!”

“英雄笔在未来十年必将消亡的 10 个原因”

“新的写作模式,双手梅花篆字”

“怎么在一分钟之内写 300 个字——论英雄没有关勒铭流畅的原因”······

可以想象是个什么场景吗?

是的,我当然知道把程序员比做作家是牵强附会并不合适,但是一些(甚至可以说很多)程序员在讨论、在追求的都是用什么语言,用什么框架,有的人没有科班背景只是在培训机构培训了几个月(没有任何对培训机构偏见的意思)就出来加入了轰轰烈烈的”程序员讨论“中。

这样真的有意义吗?难道写代码真的要与语言、框架绑定在一起吗?你是学 Java 的,让你写 Python 你就可以说自己不是程序员了吗?程序员从何时开始这么廉价了?语言、框架(以及目前各种 Mxx 设计模式)对于程序员不就像笔对于作家一样只是工具而已吗?为什么作家要被笔束缚?为什么程序员要被语言、框架束缚?

程序员应该是骄傲的作家,不应该沦落为体力劳动者,如果你是后者,请不要,至少不要在我面前说你自己是个程序员!

Hibernate 插入中文乱码问题

最近在做JSP大作业,因为懒得写jdbc + 封装字段了,再加上也想学习一下SSH,就使用Hibernate来做持久化。但是中间遇到了中文乱码问题:

向数据库插入中文之后,在sqlbuddy看到的结果是乱码,已经确定乱码问题一定出在Hibernate向数据库插入的过程中。然而明明已经在前端、Servlet、数据库分别设置了字符编码是UTF-8,仍然出现乱码问题。找了很多解决方案终于了解到:Hibernate自己的字符编码默认不是UTF-8,所以要在hibernate.cfg.xml中的connection.url设置如下内容:

1
<property name="connection.url">jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&amp;characterEncoding=UTF-8</property>

设置完成之后,可以正常进行中文操作。

JavaScript 中的运算操作陷阱

最近做JSP大作业,需要写一个网站,其中用到了分页系统,就自己撸了一个。

其中的翻页逻辑是这样写的:

1
2
3
4
5
6
7
8
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
clickPage(grids[currentActivePos].find('a').html() + 1);
});

扔到网页里实验,发现[上一页]功能没有问题,但是点击[下一页]之后没有反应,下边还有一个类似的错误:

1
grids[i].find('a').html(index + i - 3);

当测试时,index是从.html()方法获取的字面值为3的参数,i是循环控制变量,当i也是3时,出现了gridsp[i]的值变成30而不是3的情况。

一直想不明白这奇怪的现象是因为什么,但问题肯定就出在这句话上,所以就试着把表达式的顺序变了一下,像这样:

1
grids[i].find('a').html(i - 3 + index);

发现结果变成了03,到这里才明白这结果不是数值而是字符串!也就是index是string类型的,直接用数字和它进行+操作结果不是进行数值运算而是字符串连接!

因而也明白了翻页逻辑的错误在哪里。改动如下:

1
2
3
4
5
6
7
8
9
10
11
/*上一页*/
previousPage.click(function(){
clickPage(grids[currentActivePos].find('a').html() - 1);
});
/*下一页*/
nextPage.click(function() {
var des = eval(grids[currentActivePos].find('a').html() + '+ i');
clickPage(des);
});
/*先进行减法运算,把表达式结果转换成数值类型*/
grids[i].find('a').html(index - 3 + i);

总结:非常傻的一个错误,找到原因之后觉得自己非常基础的东西没有掌握牢固。这一段时间虽然涉及的面不少,但是都没有什么深入,这样就有点像”非科班”的CS学生了,应该警醒。

我的网站

终于找到相对靠谱的服务器了,域名:

rucer.cn

意思是:人大人.cn,果然人大是文科性质的学校,这么靠前的域名竟然没人注册。

旧版网站比较LOW,就不放上去了,之后有时间做新版。

处理运行时状态的改变

英文原文地址 http://developer.android.com/guide/topics/resources/runtime-changes.html

一些设备配置会在运行时改变(比如屏幕方向、键盘可用性和语言)。当发生这些改变时,Android会重新启动正在运行的Activity(onDestroy()会被调用,之后是onCreate())。设计重新启动行为是为了通过自动重新加载你的应用从而适应新的配置,重新加载的应用会采用适应新配置的替代资源。

要正确地处理重新启动,非常重要的一点是你的activity在正常的Activity活动周期中存储下来它之前的数据,在活动周期中,Android在销毁你的activity之前会调用onSaveInstanceState()从而你可以保存有关应用状态的数据。你可以在onCreate()或者onRestoreInstanceState()中恢复数据。

为了测试你的应用是否状态完好地重新自启动,你应该在应用进行各种任务时改变配置状态(比如修改屏幕方向)。为了能够处理诸如配置改变或者用户接了一个电话,之后在你的应用进程被销毁之后回到应用之类的情况,你的应用需要能够在保证用户数据或状态无损的情况下随时重启。如果你想知道如何恢复activity的状态,请阅读Activity lifecycle.

但是你可能会遇到这样的情况:重新启动应用并恢复大量的数据开销非常大,而且会导致糟糕的用户体验。在这种情况下,你有另外两个选择:

  1. 在配置改变期间保留一个数据对象
    允许你的activity在配置改变时重启,但是要为你的activity保留一个有状态的对象(object)。
  2. 你自己来处理配置的改变
    在特定的配置改变时阻止系统重启应用,但是当配置改变时要接收回调函数,从而你可以按照需求手动地更新的你的activity。

在配置改变期间保留一个数据对象

如果重启应用需要你恢复大量的数据,重新建立网络连接或者进行其他密集型操作,那么由于配置改变而造成的一次完整的重启可能会是一次非常慢的用户体验。除此之外,你可能也无法使用系统通过onSaveInstanceState()回调函数为你保留的Bundle完整地恢复你的activity状态——它并非是被设计用来承载比较大的对象(比如bitmaps)的,而且其中的数据必须被序列化和反序列化,这些操作会占用非常多的内存并且使得配置切换缓慢。在这种情况下,你可以通过持有一个Fragment来减轻因为配置改变而重启activity时的负担。这个fragment将会保存你想要留存的有状态的对象的引用。

当安卓系统因为配置改变而关闭你的activity时,你标记为保留的fragment不会被销毁。你可以在你的activity中添加这样的fragment来保存有状态对象。

要想在运行配置改变期间在fragment中持有有状态对象:

  1. 继承Fragment类并声明对你的有状态对象的引用。
  2. 当fragment创建时调用setRetainInstance(boolean)。
  3. 把这个fragment添加到你的activity中。
  4. 当activity重启之后通过FragmentManager获取这个fragment。

举个例子,像下边这样定义你的 fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RetainedFragment extends Fragment {

// data object we want to retain
private MyDataObject data;

// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}

public void setData(MyDataObject data) {
this.data = data;
}

public MyDataObject getData() {
return data;
}
}

小心:虽然你可以存储任何对象,但是永远也不要传递一个绑定在Activity上的对象,比如Drawable,一个Adapter,一个View或者任何一个关联到Context上的对象。如果你这么做的话,会导致原有activity实例所有视图和资源的泄露。(资源泄漏是指你的应用一直挂起这些资源使他们无法被垃圾回收,因而很多内存会流失掉。)

然后使用FragmentManager把fragment添加到activity。当activity在配置改变期间重新启动后你可以从这个fragment获取数据对象。例如,像下边这样定义你的activity:

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
public class MyActivity extends Activity {

private RetainedFragment dataFragment;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}

// the data is available in dataFragment.getData()
...
}

@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}

在这个例子中onCreate()添加了一个fragment或者恢复了对它的引用。onCreate()也在fragment实例中存储了有状态对象。onDestroy()更新了持有的fragment中的有状态对象。

你自己来处理配置的改变

如果你的应用在特定的配置改变时不需要更新资源并且由于性能限制不允许重启activity,那么你可以声明你的activity自己来处理配置改变,这样可以阻止系统重启你的activity。

标注:亲自处理配置改变将会比使用可选资源困难得多,因为系统不会自动为你应用这些资源。当你一定要禁用由于配置改变而造成的重新启动时,这项技术可以被看做最后的手段,对于大多数应用并不推荐。

为了声明你的activity要处理一项配置更改,在你的manifest文件中编辑合适的标签来修改android:configChanges属性,填入合适的值以代表你要处理的配置。可选的值列在了android:configChanges属性的文档中(最常用的值是”orientation”来避免屏幕方向改变时activity的重启,以及”keyboardHidden”来避免键盘可用性发生改变时activity的重启)。你可以通过使用 | 把值隔开的方式声明多个值。

举个例子,下边的manifest代码声明了一个同时处理屏幕方向改变和键盘可用性改变的activity。

1
2
3
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">

现在,当这些配置的其中一个发生改变时,MyActivity不会重启,而是接收到了一个对onConfigurationChanged()的调用。这个方法传递了一个Configuration对象,指定了新的配置。通过读取Configuration中的字段,你可以定义新的配置并且通过更新你的界面所用资源来做出合适的修改。当这个方法被调用时,你的activity的Resources对象会被更新并返回基于新配置的资源,从而你可以在系统不重启你的activity的情况下重置你的UI的元素。

注意:从Android3.2(API 版本13)开始,当设备在横屏和竖屏之间进行切换时”screen size”也会发生变化。因此在做API版本13或者更高(在属性minSdkVersion和targetSdkVersion中声明)的开发时,如果你想避免因为屏幕方向改变而引起的重启,你必须在”orientation”的基础上包含”screenSize”属性。也就是说,你必须声明android:configChanges=“orientation|screenSize”。但是如果你的应用是面向API版本12或者更低的,那么你的activity总能自己处理这种配置改变。(即使运行在Android版本3.2或者更高的设备上,配置改变也不会引起activity重启)

例如,下边的onConfigurationChanged()实现检查了当前设备的屏幕方向:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}

Configuration对象表示出了目前所有的配置,不仅仅是已经改变了的。大部分情况下,你并不关心你的配置是如何改变的以及它是如何简单地重新分配你的资源以达到为你正在处理的配置提供备选的目的。例如,因为Resources对象现在已经更新,你可以通过setImageResource()重新设置ImageView,你也可以设置其他对已经使用的配置来说合适的资源(就像Providing Resources说的那样)。

注意Configuration中的字段都是整型,并且他们都与Configuration类中的常量相匹配。你可以参考Configuration中具体的部分来获取每个字段使用的常量。

记住:当你声明你的activity以处理配置改变时,你有责任重新设置你为之提供备选的每个元素。如果你声明你的activity来处理屏幕方向变化并且有需要在横屏和竖屏之间切换的图片,你必须在onConfigurationChanged()中为每个元素重新分配新的资源。

如果你不需要基于这些配置修改做activity的更新,你可以不去实现onConfigurationChanged()。在这种情况下,配置更改前后使用的资源是相同的,你仅仅是避免了activity的重新启动。但是,你的应用应该总是能够关闭后再打开时原样保留之前的状态。因此,在正常的activity生命周期内,你不应该把这项技术当成保留状态的选择。不仅是因为你不能阻止一些其他的配置改变引起的应用重启,而且你还应该处理诸如用户离开应用,在用户再次开启应用之前应用就被销毁的情况。

想了解更多关于你在activity中可以处理的配置改变,请查阅android:configChanges文档和Configuration类。

Python 进行 Web 开发

假期没事学python,因为之前一直在搭个人网站,后台用的是PHP,所以想用Python重写一下后台。关于python开发web应用,网上有一大堆教程,最多的是推荐用apache加载mod_python这个模块,看了下官网,13年停止更新了,而且对Python 3的支持很差,所以并不推荐这个。类似的有mod_msgi,这个可以看做是mod_python的继任者,但是配置起来尤其是在windows上配置起来非常麻烦,初学者做起来可能会比较痛苦。推荐使用Django框架开发,关于服务器官网里有这么一段话:

You’ve started the Django development server, a lightweight Web server writtenpurely in Python. We’ve included this with Django so you can develop thingsrapidly, without having to deal with configuring a production server – such asApache – until you’re ready for production.

也就是Django自带了纯python编写的服务器,非常方便,在测试阶段和小规模的开发阶段,用这个足以满足需求。关于教程,强烈推荐去官网硬着头皮读英文原版(注意教程版本要和自己下载的Django一致)而不要去看翻译完的中文版,因为Django更新很快,而且时不时有大的变动,看比较老的版本的话有时候回给自己造成莫名其妙的困扰。

Django官网:https://www.djangoproject.com/

Python下载:https://www.python.org/

下载完之后,先把python.exe的目录放到环境变量的Path下(Windows系统),然后命令行进入Django目录,执行 [ python setup.py install ] 即可。安装Django成功之后,在Django官网中按照教程一步步来就可以了。

Android ListView 和 ScrollView 冲突问题

最近做一款APP,其中有一个类似微博的评论功能的界面,先是列出微博的正文内容和图片等,然后下边是评论。一开始就想着用一个ScrollView把主要内容和评论区的ListView包起来,然后添加各个控件的内容即可(对,感觉上有点像这个CSDN博客的编辑界面嘛),但是写出来之后发现ListView只显示出了一个条目的高度,并且不能滑动,网上搜了一下发现原因是ScrollView和ListView都是可滑动的,把它们放在一块会有冲突,最后还是ScrollView获得了焦点,ListView不能滑动。网上的解决方法最多的是在加载ListView时用getMeasure计算每个条目和分割线的高度,然后相加,把结果设置为ListView控件的高度,不过貌似是只适用于ListView每个条目高度都一样的情况(没有试过,很奇怪为什么会这样)。要么就是自定义一个继承自ListView的控件,也是事先设置好ListView的高度,但这样总归比较麻烦,而且准确度不如由系统自己构造好。

懒癌发作实在不想自己去做这些事情,于是便想试一下比较投机的方法,就是在ListView的Adapter的getView方法中根据position构造不同的界面,即如果position是0,则用原来主要信息(微博正文,图片)的xml文件取inflate convertView,否则就用评论条目的xml去inflate,经试验果然可行。之后不死心想看下有没有更好的实现方法,去overflow上找了一下,发现有人推荐的方法和我的差不多,所以认为这种方法是比较好的,不需要做额外的工作,只需要把inflate的工作由主Activity放在Adapter里就可以了。

getView方法

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
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
/*主信息界面*/
if(0 == position)
{
MainHolder holder = null;
convertView = inflater.inflate(R.layout.info, parent, false);
holder = new MainHolder();
convertView.setTag(holder);
······
······
}
/*评论界面*/
else
{
ItemHolder holder = null;
convertView = inflater.inflate(R.layout.item, parent, false);
holder = new ItemHolder();
convertView.setTag(holder);
······
······
return convertView;
}
}