新年快乐
项目视频展示
APK下载 提取码:d7u8
开发 约吧 遇到的坑即解决方法:
listview的item回收问题
,这也是我去面试被提问到的问题,现在做到这部分豁然开朗。- listview为了保证item的条目不至于太多导致OOM(OutOfMemory),会自动回收,而这个自动回收机制
让我在滑动list的时候,已经加载过的item如果被滑上去屏幕之外,重新滑下来的时候又会重新加载,这个加载对于大图就显得有些卡顿
;而对于recyclerView,也有同样的自动回收机制,但不同于listview的是,recyclerView允许我们自己设置缓存的item数: recyclerView.setItemViewCacheSize(20); 让我们保证少数的item不至于每次滑动之后都要销毁,但是这样的机制也是不可避免的,为了保证内存不会OOM,所以我需要考虑的是另外一个问题,就是加载图片减小其大小即可提升加载速度。
- listview为了保证item的条目不至于太多导致OOM(OutOfMemory),会自动回收,而这个自动回收机制
- 提升加载图片的速度,就需要我去思考怎么缩小加载图片的大小。解决思路,使用Glide加载图片时加上
fitCenter()
或者修改imageView的scaletype为fitCenter或者centerCrop即可。(缩放显示图,分辨率再大也按imageview的size来加载)- 对比:
- 一开始不作处理直接加载大图,加载时间很长,用户体验不是很好:
- 改用fitCenter加载大图
- 明显看到分析器的Energy列的差距,使用fitCenter让加载所消耗的资源减少了许多。
- 解决方法参考
- 对比:
-
一个非常坑的东西,在fragment中不能直接startActivityForResult,因为这个的requestcode是随机生成的,切记应该需要先获取activity,再使用该指令,如下:这样才能在MainActivity中获取到正确的requestCode getActivity().startActivityForResult(intent,FINISH_POST);
- 又一个非常非常坑的东西,应用刚启动时,便会调用所有fragment的setUserVisibleHint()函数一次,关键是,fragment创建的时机并不是在activity的oncreate方法之后。
- 针对性的,我出现这个问题是因为我想在fragment创建的时候便初始话一次postList,如果我们在setUserVisibleHint函数里一直调用刷新postList的话,会让用户体验不是很好,做一些多余的刷新操作,那么我这里就很巧妙地利用了上面提到的fragment的生命周期超前于activity的特点:判断
getActivity()!=null
,同时设一个boolean值,这样就完成了只有一次的初始化操作。@Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (init && getActivity()!=null){ init = false; refresh(); } }
- 关于
setUserVisibleHint
函数的非常好的参考博客 - 关于fragment的生命周期,非常重要的几个结论:
-
fragment生命周期图:
问:
- fragment是如何知道自己时候用户可见?
- setUserVisibleHint() 在上图所示fragment的生命周期的什么位置?
答:
- viewpager监听切换tab事件,tab切换一次,执行一次setUserVisibleHint()方法
- setUserVisibleHint() 在 上图所示fragment所有生命周期之前,无论viewpager是在activity哪个生命周期里初始化。
- activity生命周期 和 fragment生命周期 时序并不是按序来的,也就是说fragment的oncreate方法时序并不一定在activity的oncreate方法之后。
-
- 针对性的,我出现这个问题是因为我想在fragment创建的时候便初始话一次postList,如果我们在setUserVisibleHint函数里一直调用刷新postList的话,会让用户体验不是很好,做一些多余的刷新操作,那么我这里就很巧妙地利用了上面提到的fragment的生命周期超前于activity的特点:判断
-
单独刷新recyclerView中某个item:adapter.notifyItemChanged
- 给toolbar 上的item添加长按事件,不能直接set监听器,需要在线程中set:
@Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.post_toolbar, menu); new Handler().post(new Runnable(){ @Override public void run(){ View menu_more=getActivity().findViewById(R.id.pick_image); menu_more.setOnLongClickListener(new View.OnLongClickListener(){ @Override public boolean onLongClick(View v){ Toast.makeText(getActivity(),"123123",Toast.LENGTH_SHORT).show(); return true; } }); } }); }
-
R爆红的问题,这是经常容易在开发过程中突然发生的现象,刚刚解决了,是xml文件中有引入了一个图片,但是我直接把这个图片删了,就导致了R命名文件的错乱,以后遇到这问题排查一下找到删掉文件之前是否被使用过即可。
- retrofit使用相关参数详解
-
网络请求方法
-
网络请求规则
-
标记
-
网络请求参数
-
- recyclerView的item局部刷新
notifyItemChanged(int position, @Nullable Object payload)
回调onBindViewHolder(@NonNull VH holder, int position,@NonNull List<Object> payloads)
方法
- 下拉刷新:安卓自带的下拉刷新布局:SwipeRefreshLayout
- SwipeRefreshLayout使用详解
- 几种强大的下拉刷新库
- 使用SwipeRefreshLayout记得里面只能有单一的listView或者recyclerView,有其他组件的话会导致下拉布局不能正确监控list的下滑动作。
- adater适配器的问题,在做评论的删除回复部分时,我发现回复删除操作之后,adapter都无法立即更新,但是postman获取下来的post信息中,是更新了的,说明
commentAdapter.notifyDataSetChanged();
这条指令没有起作用。- 一开始为了方便,我是把commentAdapter作为一个私有成员变量来使用的,当我把删除回复评论的函数进行修改,改为传入一个commentAdapter 参数来使用,而不是直接使用私有成员变量,这样不论在哪里创建的adapter都能被正确传入对应的触发函数中,保证了适配器的准确性。
private void showCommentDeleteDialog(final CommentAdapter commentAdapter, final int commentId, final int postIndex, final int commentIndex) ······
- 后来发现,私有成员变量的方式其实并没有问题,是我
setAdapter(commentAdapter);
的时机不对,在点击事件发生之后,才把适配器set上去,这样的适配器肯定无法与其绑定的数据保证实时更新。
- 一开始为了方便,我是把commentAdapter作为一个私有成员变量来使用的,当我把删除回复评论的函数进行修改,改为传入一个commentAdapter 参数来使用,而不是直接使用私有成员变量,这样不论在哪里创建的adapter都能被正确传入对应的触发函数中,保证了适配器的准确性。
- Android 布局平铺展开效果的属性动画
-
修改成我们需要的从左至右平铺的一个动画之后,有个bug,平铺之前会闪烁一下,用户体验不是很好。经过排查找到罪魁祸首,就是setVisibility为Visible的时候,会先让要显示的两个按钮完整的显示出来,然后才开始执行动画。
- 琢磨了一下,终于找出问题所在:
v.setLayoutParams(layoutParams);
,原来的实现方式如下ValueAnimator animator = createDropAnimator(view, 0,origHeight); private ValueAnimator createDropAnimator(final View v, int start, int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator arg0) { int value = (int) arg0.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = v.getLayoutParams(); layoutParams.width = value; v.setLayoutParams(layoutParams); } }); return animator; }
- setLayoutParams只是一种临时修改layout的长款的方法,其实对于要被显示的内容,只是一种临时的修改,是没有被存下来的。所以我思考之后,把布局伸展的动画改用padding来实现,这样就不会有突然闪烁的糟糕体验了。
ValueAnimator animator = createDropAnimator(view, 0,origHeight); private ValueAnimator createDropAnimator(final View v, int start, int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator arg0) { int value = (int) arg0.getAnimatedValue(); v.setPadding(value,0,0,0); } }); return animator; }
-
- 获取时区,我们应用内的时间是根据用户所在时区显示的,而java中获取当前时区的方法是在麻烦,查资料的结果试了很多次都不行,最后终于找到有效的方法:
public static String toLocalTime(String utcTime) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); int offset = 8; Date utcDate = null; try { utcDate = simpleDateFormat.parse(utcTime); } catch (ParseException e) { e.printStackTrace(); } TimeZone tz = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // 如果SDK版本大于26, 判断用户的时区进行转换, 否则默认为8 tz = TimeZone.getTimeZone(ZoneId.systemDefault()); offset = tz.getRawOffset()/(60*60*1000); } Calendar calendar = Calendar.getInstance(); calendar.setTime(utcDate); calendar.add(Calendar.HOUR, offset); utcDate.after(new Date(offset)); String localTime = simpleDateFormat.format(calendar.getTime()); return localTime; }
-
一个比较坑的地方就是,我们的虚拟机默认时区是GMT,格林尼治时间,所以导致我们一直以为是获取时区的方法有问题,设置好我们对应的时区之后,就可以成功显示GMT-8的时间了,设置步骤:
-
成功效果如下:
在此基础上,修改时间显示的方式
,如果社区的post发表时间在当前时间的两天以前之内,就显示时间为多久之前
:public static String toLocalTime(String utcTime) { String localTime = utcTime; SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); int offset = 8; Date utcDate = null; try { utcDate = simpleDateFormat.parse(utcTime); } catch (ParseException e) { e.printStackTrace(); } TimeZone tz = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // 如果SDK版本大于26, 判断用户的时区进行转换, 否则默认为8 tz = TimeZone.getTimeZone(ZoneId.systemDefault()); offset = tz.getRawOffset()/(60*60*1000); } //当前时间 Calendar cal = Calendar.getInstance(); Date currentDate = cal.getTime(); int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH ) + 1;//获取到0-11,与我们正常的月份差1 int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); //计算传入时间与当前时间的差 cal.setTime(utcDate); cal.add(Calendar.HOUR, offset); utcDate.after(new Date(offset)); Date pre_date = cal.getTime(); int pre_Year = cal.get(Calendar.YEAR); int pre_month = cal.get(Calendar.MONTH ) + 1;//获取到0-11,与我们正常的月份差1 int pre_day = cal.get(Calendar.DAY_OF_MONTH); int pre_hour = cal.get(Calendar.HOUR_OF_DAY); int pre_minute = cal.get(Calendar.MINUTE); //若传入时间在当前时间的两天之内,则显示多久以前 cal.add(Calendar.DAY_OF_MONTH,2); Date temp = cal.getTime(); if(temp.after(currentDate)){ if(day == pre_day){ if(hour == pre_hour){ if(minute == pre_minute) localTime = "1分钟前"; else localTime = (minute - pre_minute) + "分钟前"; } else { if(pre_hour>=12){ localTime = "下午"+ pre_hour + ":" ; } else localTime = "上午" + pre_hour + ":" ; if(pre_minute < 10) localTime += "0" + pre_minute; else localTime += pre_minute; } } else{ if(pre_hour>=12){ localTime = "昨天 下午"+ pre_hour + ":" ; } else localTime = "昨天 上午" + pre_hour + ":" ; if(pre_minute < 10) localTime += "0" + pre_minute; else localTime += pre_minute; } return localTime; } simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); localTime = simpleDateFormat.format(pre_date); if(pre_hour >= 12){ localTime += " 下午"+ pre_hour + ":" ; } else localTime += " 上午"+ pre_hour + ":" ; if(pre_minute < 10) localTime += "0" + pre_minute; else localTime += pre_minute; return localTime; }
-
最终实现效果如下:
-
Fragment切换时不断的调用onCreateView
函数导致重复刷新fragment
的问题:
- 这是一个非常好的问题,fragment在切换时会不断的调用
onCreateView
函数导致重复刷新fragment
造成不必要的流量浪费,影响用户体验。 - 这是fragment的机制——每次切换fragment的时候,Fragment都会重新实例化、重新执行onCreateView()方法、重新加载一边数据,这样非常消耗性能和用户的数据流量。所以,如何让多个Fragment彼此切换时不重新实例化?
- 解决方法:
将第一次创建的view缓存下来
:当第一次创建fragment的时候在onCreateView里面初始化view,下一次创建时不需要重新创建view时,希望使用已经创建的,所以要把view设为全局变量。view为空,表示是第一次,则初始化view。如果view不为空,则返回该view,需要注意的是:如果直接返回会报错(java.lang.IllegalStateException: The specified child already has a parent),大体意思就是有一个parent了,所以在返回该view前要找到该view的parent,然后remove掉该view,再返回就ok了。private View rootView;//缓存Fragment view @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if(rootView==null){ rootView=inflater.inflate(R.layout.tab_fragment, null); } //缓存的rootView需要判断是否已经被加过parent, 如果有parent需要从parent删除,要不然会发生这个rootview已经有parent的错误。 ViewGroup parent = (ViewGroup) rootView.getParent(); if (parent != null) { parent.removeView(rootView); } return rootView; }
- Fragment 有onCreate()和onCreateView()方法,onCreate方法是在类创建时调用,而onCreateView方法是在Fragment布局显示的时候才会调用,Fragment布局中的控件在onCreateView中可以通过view.findViewById方法获取,但不能在绑定的Activity中获取,因为Activity中通过findViewById方法获取到的控件必须是在setContentView()布局中,其他布局中的控件无法获取。
- 注意:
- Fragment中onCreate类似于Activity.onCreate,在其中可初始化除了view之外的一切;
- onCreateView是创建该fragment对应的视图,其中需要创建自己的视图并返回给调用者;
- Fragment生命周期问题
- 解决方法: