Day05 01.昨天内容总结 ##
Day05 02.头条新闻轮播&事件处理 ##
首页轮播条自动切换功能,在自定义控件时已经实现过
1.用handler实现头条新闻轮播条ViewPager自动切换的功能(用handler发延时消息,在消息中又去发消息形成内循环)
TabDetailPager中new一个handler并重写handleMessage方法
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
};
};
设置头条新闻也就是第一页数据初始化时启动自动轮播效果
TabDetailPager的“if (!isMore) {// 第一页数据初始化"中启动自动轮播
用handler去sendEmptyMessageDelayed(what,delayMills),延时2秒发一个消息
// 启动自动轮播效果
mHandler.sendEmptyMessageDelayed(0, 2000);
这样去写会有小问题
希望轮播条只启动一次,但现在有可能被启动多次
用户一下拉刷新,又重新拉了一遍数据,重新调用processData加载第一页数据
processData中调用了mHandler.sendEmptyMessageDelayed(0, 2000)启动自动轮播
相当于又发了一遍消息重新启动了一次自动轮播效果,发了之后在消息中又去发
已经把这个自动轮播启动了,再启动会导致消息很多,上次发的消息和这次发的消息会有冲突,导致轮播条错误
2.解决轮播条可能被启动多次的问题
保证handler永远只发送一次
不再成员变量处初始化handler,在成员变量处声明为null
private Handler mHandler = null;
processData的isMore判断中去初始化,判断只有在handler为空时才去new一个handler对象
在handler对象中实现handleMessage方法,并在handleMessage方法外边并且在此判断中sendEmptyMessageDelayed发消息,不要一上来就在成员变量处实现
// 启动自动轮播效果, 只需要启动一次
if (mHandler == null) {
mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
int currentItem = mViewPager.getCurrentItem();
currentItem ++;
mViewPager.setCurrentItem(currentItem);
Handler.sendEmptyMessageDelayed(0, 2000);
};
};
mHandler.sendEmptyMessageDelayed(0, 2000);
}
第一次进来,TabDetailPager刚初始化完成,mHanlder是空,一调用processData加载数据就不为空了
它走到if(mHandler == null)中new了handler,下次再进来mHandler就不为空了,不为空就不会再进这个判断
在handleMessage外边并且在此判断中调用mHandler.sendEmptyMessageDelayed(0, 2000)方法,相当于handler只发送了一次
启动完handler后,在handleMessage方法中更改下viewPager位置
用mViewPager.getCurrentItem()拿到一个int类型的currentItem,然后currentItem去++递增
递增后让mViewPager去setCurrentItem,应该等于目前的currentItem
这样去写还会有一个小问题
这个界面只有4个轮播图,到第4个再跳第5个肯定跳不过去,永远都是第4个
上次取余是因为这个是好多个整数的最大值,所以不是取余不取余的问题
3.当轮播条是最后一个页面时下一个页面应该是第一个页面的实现(判断当前页是否小于ViewPager总大小-1)
item值在这个界面只可能是0,1,2,3,需要发现是最后一个时,下一个应该是第1个页面,不应该是第4个页面
4.获取ViewPager总大小的2种方式
要判断当前页面是否小于ViewPager总大小
TopNewsAdapter中getCount方法返回的mTopNewsList.size()就是ViewPager的大小
viewPager的getCount方法有问题,不要拿getCount,除非用Adapter引用去拿,但TopNewsAdapter此处没有声明成全局,所以这里不用引用这种了
如果currentItem小于mTopNewsList.size()-1,说明没到最后一个(第4个),就要跳到下一个页面
else已经到最后一个页面了,currentItem应该等于0
if (currentItem < mTopNewsList.size() - 1) {
currentItem++;
} else {// 如果到最后一个页面了, 那下一个页面就回到第一个页面
currentItem = 0;
}
运行程序,看到轮播图隔3秒就跳到下一个页面,再隔3秒,还要跳到下一个页面,但是没跳,没跳是因为没发消息
mViewPager.setCurrentItem(currentItem);下边,还需要用mHandler再去发一个消息,即添加代码: mHandler.sendEmptyMessageDelayed(0, 2000);
运行程序,看到隔2秒就跳下个页面,再隔2秒再去跳下一个页面,到第4个页面后,隔2秒,就会跳转到第一个页面
5.实现滑动轮播条时停止自动切换的功能(给ViewPager设置触摸监听setOnTouchListener)
滑动轮播条时不希望自动去切,可以给ViewPager设置触摸监听
在if(mHandler == null)判断的下边给mViewPager设置setOnTouchListener,并重写它的onTouch方法
被触摸时需判断触摸事件event.getAction
ACTION_DOWN按下后要移除mHandler中所有的消息
mHandler.removeCallbacksAndMessages(null);// 移除所有消息,停止轮播效果
ACTION_UP抬起后要发消息让轮播图继续自动轮播
mHandler.sendEmptyMessageDelayed(0, 2000);// 继续轮播
最后return一个false
6.实现滑动轮播条时触摸监听的onTouch返回false的原因
因为ViewPager自身也有滑动,它自己也要滑动,如果return true它自身就滑动不了了
mViewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
System.out.println("ACTION_DOWN");
mHandler.removeCallbacksAndMessages(null);// 移除所有消息,停止轮播
break;
case MotionEvent.ACTION_CANCEL:
//事件取消, 当按下viewpager后,又开始滑动listview, 会触发事件取消的事件
System.out.println("ACTION_CANCEL");
mHandler.sendEmptyMessageDelayed(0, 2000);// 继续轮播
break;
case MotionEvent.ACTION_UP:
System.out.println("ACTION_UP");
mHandler.sendEmptyMessageDelayed(0, 2000);// 继续轮播
break;
default:
break;
}
return false;
}
});
运行程序,在自动轮播,当我去按住时会走到ActionDown中,这时就不会自动轮播了,按住去滑动可以滑动,抬起手就会走到ActionUp中就又会自动轮播了
按下轮播图上下滑动整体ListView(普通新闻),抬起时,不会走ViewPager的ActionUp,轮播图就不会自动轮播
7.解决按下ViewPager后上下滑动导致事件被ListView处理后不会走到ViewPager的ActionUp导致ViewPager不在自动轮播的问题(使用ViewPager的取消事件ACTION_CANCEL)
这个地方有很多人在处理事件,有一个苹果,listview,ViewPager都盯着,按下之后还滑动,上下滑动时让ListView把这个事件消费掉了
还有一个事件MotionEvent.ACTION_CANCEL,本来要点击,现在又不想点击了,事件取消了,这就是取消事件
取消的事件在按下viewPager后,又开始滑动listview,让listview去处理事件了,这时会触发‘取消事件’事件
取消后还要自动轮播,在case MotionEvent.ACTION_CANCEL中添加如下代码
mHandler.sendEmptyMessageDelayed(0, 2000);// 继续轮播
运行程序,按下,抬起轮播图,没问题,按下轮播图后上下滑动,马上会打印ACTION_CANCEL,CANCEL后轮播图又自动轮播了
8.点击轮播图中的imageView跳转到新闻详情页的实现(和listview给某个item设置图片的点击事件一样)
这个点击事件写在了TabDetailPager的TopNewsAdapter的instantiateItem中有个ImageView,ImageView一被点击就跳到相关页面
在instantiateItem中给ImageView(view)设置点击事件,打印日志‘头条新闻被点击’
就这么简单,所以不要问ViewPager轮播图的点击事件怎么去设置,点击事件的设置永远是setOnClickListener
运行项目,因为ImageView是ViewPager的子类,不确定Viewpager是否会把ImageView点击事件抢掉
点击轮播图的ImageView看到日志中打印"头条新闻被点击",说明没有被抢掉
点击要跳转到新闻详情页,在instantiateItem的参数中已经有position了,在头条新闻那拿到对应的头条新闻topNews
TopNews topNews = mTopNewsList.get(position);
头条新闻也有它的新闻详情页,传的positon报错是因为setOnClickListener是内部类
内部类访问instantiateItem的参数position有问题,ctrl+1把instantiateItem的参数position变为final就行了
上次做了ListView跳页面,把那个抄过来,把url改为头条新闻的topNews.url,也跳到新闻详情页
ViewPager轮播图跳转新闻详情页具体代码如下:
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("头条新闻被点击");
TopNews topNews = mTopNewsList.get(position);
// 跳到新闻详情页
Intent intent = new Intent();
intent.putExtra("url", topNews.url);// 将url参数传递到下个activity
intent.setClass(mActivity, NewsDetailActivity.class);
mActivity.startActivity(intent);
}
});
运行程序,点击头条新闻ViewPager的某个具体ImageView就会跳转到新闻详情页
Day02 03组图数据获取&列表展示 ##
完整项目,到新闻中心点击侧边栏中组图,组图上边是图片,下边是文字,是用ListView做的
组图的标题栏右上角有个按钮,点击后变成两行两列的图片,还是上边是图片,下边是文字,是用GridView做的
组图数据获取和展示方式切换的实现(把ListView和GridView用帧布局压在一起,一个显示一个隐藏就行了)
PhotoMenuDetailPager中实现组图模块,写组图的布局文件pager_photo_menu_detail.xml,里边就是用FrameLayout把id为lv_photo的ListView和id为gv_photo的GridView压在一起,给GridView设置numColumns属性为2,默认gone掉
pager_photo_menu_detail.xml此处略
PhotoMenuDetailpager的initView中用代码写的TextView删除后加载布局,并用ViewUtils初始化控件
@ViewInject(R.id.lv_photo)
private ListView lvPhoto;
@ViewInject(R.id.gv_photo)
private GridView gvPhoto;
View view = View.inflate(mActivity, R.layout.pager_photo_menu_detail,null);
ViewUtils.inject(this, view);
这个数据还是要调接口,TomCat服务器Root/zhbj/photos/photos_1.json,这是项目调的第3个接口
浏览器地址栏输入localhost:8080/zhbj/photos/photos_2.json,回车,看到接口返回的数据
data下边是news,news中是所有组图对应的详细数据
把接口链接抄到全局的GlobalConstants中,把接口声明下,组图接口叫做PHOTOS_URL
// 组图信息接口
public static String PHOTOS_URL = SERVER_URL + "/photos/photos_1.json";
在组图菜单详情页PhotoMenuDetailPager中初始化数据的initData中调用getDataFromServer()从服务器获取数据
然后生成getDataFromServer(),还是new个HttpUtils请求网络
utils去set请求,获取数据一般都是GET请求,url是GlobalConstants.PHOTOS_URL
callback去new一个RequestCallBack<String>,范型是String,重写成功和失败方法
private void getDataFromNet() {
HttpUtils utils = new HttpUtils();
utils.send(HttpMethod.GET, GlobalConstants.PHOTOS_URL,new RequestCallBack<String>() {
@Override
public void onSuccess(ResponseInfo<String> responseInfo) {
String result = responseInfo.result;
processData(result);
}
@Override
public void onFailure(HttpException error, String msg) {
// 请求失败
error.printStackTrace();
Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT).show();
}
});
}
}
从新闻中心NewsCenterPager中抄下请求失败的错误信息提示
请求成功从onSuccess的参数responsseInfo中拿到String类型的result就是它的json数据
再写一个方法processData()用Gson解析result
整体解析json的方法如下:
private ArrayList<PhotoNews> mNewsList;
protected void processData(String result) {
Gson gson = new Gson();
PhotoBean bean = gson.fromJson(result, PhotoBean.class);
mNewsList = bean.data.news;
//listview和gridview布局样式一样,可以共用一个adapter
lvPhoto.setAdapter(new PhotoAdapter());
gvPhoto.setAdapter(new PhotoAdapter());
}
使用Gson解析json数据需要写一个bean对象的,叫做PhotoBean
用浏览器打开的接口中看到,有data对象把它叫做PhotoData,retcode不要也行
data对象中有如下字段
more字段:下一页链接(String类型)
news字段:新闻(ArrayList类型)
这个地方不做分页处理了,不做分页就不用写more字段了,直接从news开始解析,news字段是一个集合,集合中又是一个对象PhotoNews,在PhotpNews中可以拿到id,还有2个链接, listimage链接(本地链接),largeimage链接(是在线的链接),还有发布时间字段pubdate,标题字段title,url字段用于点击后跳新闻页面
跳新闻页面逻辑不实现了,只是展现下组图,没有实现点击效果
PhotoMenuDetailPager的processData中,用gson去fromJson(json,classOfT);
参数json:是我们传过来的json数据result
参数classOfT:是我们刚才封装的PhotoBean对象
返回的是解析后的PhotoBean对象叫做bean
要的是bean的data的news字段,搞成全局方便在其他地方设置mNewsList数据
在PhotoAdapter中
getCount: return mNewsList.size();
getItem: return mNewsList.get(position);
getItemId: return positon;
getView:写一个ViewHolder,将控件都声明进来
static class ViewHolder {
public TextView tvTitle;
public ImageView ivPic;
}
getView中convertView为空时加载item布局文件list_item_photo.xml
if (convertView == null) {
convertView = View.inflate(mActivity, R.layout.list_item_photo,null);
}
组图item的效果图是卡片效果cardView,内部标签有个白色背景图片,图片边框有阴影(黑色线)
分别给图片和标题加id为iv_pic,tv_title
将上边的ViewHolder去new一下初始化出来,用convertView去findViewById加载控件给holder
给convertView去setTag,把holder塞给convertView
convertView不为空的else中从convertVew中getTag取出holder
getItem(position)拿到当前网络对象PhotoNews后就可以给相应控件设置数据了
在PhotoAdapter中写PhotoAdapter构造方法去new一个BitmapUtils,抽取成全局变量,并设置加载的默认图片
//private BitmapUtils mBitmapUtils;
private MyBitmapUtils mBitmapUtils;
public PhotoAdapter() {
//mBitmapUtils = new BitmapUtils(mActivity);
//mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);//设置默认加载的图片
mBitmapUtils = new MyBitmapUtils();
}
getView中,用mBitmapUtils加载图片,最后将convertView返回
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(mActivity, R.layout.list_item_photo,null);
holder = new ViewHolder();
holder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);
holder.ivPic = (ImageView) convertView.findViewById(R.id.iv_pic);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
PhotoNews item = getItem(position);
holder.tvTitle.setText(item.title);
mBitmapUtils.display(holder.ivPic, item.listimage);
return convertView;
}
在processData中给lvPhoto设置adapter为PhotoAdapter
lvPhoto.setAdapter(new PhotoAdapter());
运行程序,查看组图效果,发现listView滑动时整体会出现黑色背景
pager_photo_menu_detail.xml中给listview设置cacheColorHint值为白色#fff
同时listView默认每个item之间有黑线,把它去掉,到pager_photo_menu_detail.xml中给listView设置divider属性值为@null,即:android:divider="@null"
这是组图部分的listview展示,下节课实现GridView的展示
Day05 04.组图展示方式切换 ##
组图的标题栏右侧要有按钮,点击按钮才切换到GridView
组图标题栏切换按钮的实现(两张图片切换)
1.到titleBar.xml布局,在右侧加一个id为btn_display的imageButton按钮
这个按钮有两张图片来回去切,icon_pic_grid_type.png和icon_pic_list_type.png
布局文件中默认加载icon_pic_grid_type.png
android:src="@drawable/icon_pic_grid_type"
2.组图中怎么拿到这个埋好的按钮图片
组图页面属于新闻中心页面,新闻中心本来是basepager,在basepager中可以拿到按钮图片
basePager的initView方法中初始化这个控件,并声明成成员变量
public ImageButton btnDisplay;// 组图切换按钮
btnDisplay = (ImageButton) view.findViewById(R.id.btn_display);
3.当用户在新闻中心的侧边栏中切到新闻或组图时,把按钮展现出来的实现
新闻中心NewsCenterPager中设置当前菜单详情页setCurrentMenuDetailPager方法中切的,在这个方法中判断是否显示按钮
setCurrentMenuDetailPager中已经拿到‘获取当前被选中的菜单详情页’的pager对象了
只需判断是否是组图页面,是就显示右上角的切换按钮,否则隐藏
// 判断是否是组图页面,如果是,需要展示右上角切换按钮
if (pager instanceof PhotoMenuDetailPager) {
btnDisplay.setVisibility(View.VISIBLE);
} else {
btnDisplay.setVisibility(View.GONE);
}
4.给组图按钮增加点击事件切换组图页面展示方式(将组图按钮以构造方法的形式从新闻中心传递给组图页面)
1)将组图按钮以构造方法的形式从新闻中心传递给组图页面,然后就可以在组图页面中添加点击事件了
按钮图片属于新闻中心NewsCenterPager,可以在NewsCenterPager中初始化组图页面时把按钮图片传递给PhotoMenuDetailPager
NewsCenterPager初始化组图页面时要走PhotoMenuDetailPager的构造方法,选中PhotoMenuDetailPager构造方法,右键Open Call Hierarchy,直接就来到新闻中心NewsCenterPager页面了
给NewsCenterPager的processData中的
mMenuDetailPagers.add(new PhotoMenuDetailPager(mActivity));
增加btnDisplay参数
mMenuDetailPagers.add(new PhotoMenuDetailPager(mActivity, btnDisplay));
将按钮以对象形式传递到PhotoMenuDetailPager的构造方法,增加btnDisplay参数后会报错,按ctrl+1选择‘Change onstructor 'PhotoMenuPager(Activity)':Add...’
跳转到PhotoMenuDetailPager的构造方法中自动添加了传递过来的参数,在成员变量处声明并接收
让PhotoMenuDetailPager实现OnClickListener,ctrl+1实现onClick方法
给按钮设置OnClickListener,参数写this
private ImageButton btnDisplay;
public PhotoMenuDetailPager(Activity activity, ImageButton btnDisplay) {
super(activity);
this.btnDisplay = btnDisplay;
this.btnDisplay.setOnClickListener(this);
}
2)加标记来获取目前是GridView还是listview
onClick方法上边声明一个标记isListView,默认是true,默认展示的是listView
在onClick方法中判断是listview就显示GridView,隐藏listview,并将isListView标记置为false
否则反之
这里判断显示的ListView和GridView控件是写在pager_photo_menu_detail.xml中
id为btn_display的ImageButton是写在title_bar.xml中
这个按钮随着控件的切换也会变
如果是ListView它就是田字形图片,提示你点击田字形图片后切换到GridView
private boolean isListView = true;
@Override
public void onClick(View v) {
if (isListView) {
// 切到gridview
isListView = false;
gvPhoto.setVisibility(View.VISIBLE);
lvPhoto.setVisibility(View.GONE);
btnDisplay.setImageResource(R.drawable.icon_pic_list_type);
} else {
// 切到listview
isListView = true;
gvPhoto.setVisibility(View.GONE);
lvPhoto.setVisibility(View.VISIBLE);
btnDisplay.setImageResource(R.drawable.icon_pic_grid_type);
}
}
运行程序,点新闻中心侧边栏中的组图,默认展示的是listView,点击标题栏右上角的按钮就切到GridView了
现在GridView是空白的还没有给GridView填充数据
5.给GridView控件填充数据(ListView和GridView共用一个adapter)
给GridView填充数据,就得给它写一个Adapter,GridView和ListView可以共用一个Adapter
在给listview设置PhotoAdapter时,同时也给GridView设置PhotoAdapter
protected void processData(String result) {
Gson gson = new Gson();
PhotoBean bean = gson.fromJson(result, PhotoBean.class);
mNewsList = bean.data.news;
//listview和gridview布局样式一样,可以共用一个adapter
lvPhoto.setAdapter(new PhotoAdapter());
gvPhoto.setAdapter(new PhotoAdapter());
}
listView和GridView的item一模一样,都是图片下边是文字
只不过listview是一列展示,图片比较宽
GridView是两列展示,图片是微缩版,所以可以共用一个Adapter
运行程序,组图也有数据了,和效果图一样
用的是centercrop,它会自动裁剪,这样图片不会压缩不会失帧
Day05 05.三级缓存之网络缓存&AsyncTask的使用 ##
到这里zhxa项目已完全开发完了,剩下的就都是一些周边东西
1.BitmapUtils框架功能
使用bitmapUtils加载图片非常方便,组图PhotoMenuDetailPager中用BitmapUtils就可以直接将图片从网络加载下来
页签详情页TabDetialPager中用bitmapUtils时加了注释(在TabDetialPager的instantiateItem中)
Bitmaputils框架有如下功能:
1.根据图片url下载图片
2.将图片设置给ImageView
3.加了图片缓存(以图片url为文件名,以图片为文件内容缓存起来)
4.解决了内存溢出问题
2.图片的缓存机制就是三级缓存机制
页面中有很多数据,图片之类信息
图片在加载时,会优先读取内存缓存
内存缓存速度特别快,而且不会消耗流量
如果内存中没有,比如刚进来,这时就读二级缓存本地缓存,本地缓存就是sd卡缓存
本地缓存速度较快,不消耗流量
如果本地中没有,比如刚进来,这时就从网络去下载,所以第三层缓存处于云端,它是网络缓存
网络缓存速度慢,消耗流量
下载完后给本地存一下,下次就不用从网络缓存去下载了,本地加载完后要往内存存一下,下次就不用从本地去拿了
3.三级缓存流程分析
内存缓存 速度快, 优先读取
本地缓存 速度其次, 内存没有,读本地
网络缓存 速度最慢, 本地也没有,才访问网络
以组图PhotoMenuDetailPager中用BitMapUtils加载图片为例子
4.封装图片三级缓存MyBitmapUtils(BitmapUtils框架本身有这些功能)
PhotoMenuDetailPager的PhotoAdapter中注释如下代码
private BitmapUtils mBitmapUtils;
构造方法PhotoAdapter中的 mBitmapUtils = new BitmapUtils(mActivity);
加载默认图片的方法 mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
改用MyBitmapUtils
//private BitmapUtils mBitmapUtils;
private MyBitmapUtils mBitmapUtils;
public PhotoAdapter() {
//mBitmapUtils = new BitmapUtils(mActivity);
//mBitmapUtils.configDefaultLoadingImage(R.drawable.pic_item_list_default);
mBitmapUtils = new MyBitmapUtils();
}
改用MyBitmapUtils后,PhotoMenuDetailPager的getView的display报错了,因为MyBitmapUtils还没有这个方法
选中display方法按ctrl+1,选择Create method ‘display(ImageView,String)’ in type 'MyBitmapUtils’
会跳转到MyBitmapUtils中自动创建好display方法,需要传一个ImageView和url
public void display(ImageView ivPic, String url) {
}
5.封装网络缓存NetCacheUtils
第一次内存缓存和本地缓存没有,每一张图片至少要走一遍网络,所以先写网络缓存代码
在bitmap包下创建网络缓存NetCacheUtils,将MyBitmapUtils也移动到这个包下
MyBitmapUtils的构造方法中初始化网络缓存,并抽取成全局
private NetCacheUtils mNetUtils;
public MyBitmapUtils() {
mNetUtils = new NetCacheUtils();
}
MyBitmapUtils的display方法中读网络缓存,从网络下载图片需要传ImageView,url,下载后塞给imageView
mNetUtils.getBitmapFromNet(ivPic, url);// 最后读网络缓存
选中getBitmapFromNet,ctrl+1选择‘create method 'getBitmapFromNet(ImageView,String)'in type...’,会跳转到NetCacheUtils中自动创建好了这个方法
public void getBitmapFromNet(ImageView ivPic, String url) {
}
从网络加载肯定要new一个子线程去下载
平时new子线程都是new一个thread,重写run方法,子线程中要去更新主界面UI时用handler发消息去更新主界面UI
传统方式都是用“thread+handler”实现"异步数据获取及界面更新"逻辑
AsyncTask就是异步加载框架,已经做了这个功能
6.使用AsyncTask解决网络下载图片的线程切换问题(4个方法含义和使用)
创建BitmapTask继承AsyncTask,需要传3个泛型,先写成void,重写它的几个方法
需要传3个泛型,先写成void,重写下它的方法
class BitmapTask extends AsyncTask<Void, Void, Void> {
@Override
protected Bitmap doInBackground(Void... params) {
return null;
}
}
AsyncTask最典型4个方法都是回调,回调接口我们也写过
onPreExecute 预加载, 加载前的准备操作 在主线程运行的
doInBackground 开始异步加载 在子线程运行
onProgressUpdate 用来更新进度的回调,下载文件时要用到 在主线程运行
onPostExecute 加载完成更新界面UI 在主线程运行的
7.AsyncTask底层原理(AsyncTask刨开本质是线程池 + handler做的)
查看源码中有线程池THREAD_POOL,自己写了InternalHandler继承Handler
AsyncTask刨开本质是线程池 + handler做的,线程池比线程thread牛逼些,线程只有一个,线程池里可以塞很多个线程
8.NetCacheUtils的BitmapTask继承AsyncTask时的3个泛型含义和用法
1)第一个泛型,和doInBackground参数类型要一致,表示传参的类型
params参数是Void...params,参数的数量可变,表示可以传很多
传0个传10个都可以,不传就写个null,因为参数可变
把ImageView和url传过去会报错,因为指定的参数是void
public void getBitmapFromNet(ImageView ivPic, String url) {
// thread + handler
new BitmapTask().execute(ivPic, url);
}
把BitmapTask第一个参数Void改成Object,getBitmapFromNet这就不报错了,IamgeView和url就都是Object对象了
BitmapTask这会报错,因为BitmapTask的doInBackgroud的参数Void报错
getBitmapFromNet参数传到doInBackground从网络下载
doInBackground参数刚好就是getBitmapFromNet这传过来的参数
BitmapTask第一个参数改成Object了,doInBackground参数也必须是Object
所以把doInBackground参数也由Void改为Object
所以第一个泛型和doInBackground方法参数类型要一致
doInBackground中怎么把ImageView和url拿到手
底层把ivPic和url从getBitmapFromNet传到doInBackground的params参数Object[]数组中了
在doInBackground中直接取就行,这样就完成了参数的传递
数组中取出第0个是ivPic,全局变量处写个ImageView接收,Object强转成ImageView,同理接收url
private ImageView imageView;
private String url;
// 子线程运行
@Override
protected Bitmap doInBackground(Object... params) {
// 开始异步加载
imageView = (ImageView) params[0];
url = (String) params[1];
return null;
}
这是AsyncTask传参特点
传参可以不用getBitmapFromNet方法,用构造方法也可以传参
将ivPic和url写成全局变量,用doInBackground访问全局变量也可以拿到ivPic和url
只不过AsyncTask用的是这种传参方式,这三个泛型要用就要了解
2)第二个泛型,和onProgressUpdate参数类型要一致,表示更新进度的类型
把BitmapTask的第二个泛型改成整形Interger,onProgressUpdate方法这又报错了
把onProgressUpdate方法的参数也改成整形Integer就好了
3)第三个泛型,和doInBackground返回值类型要一致,和onPostExecute参数类型要一致,表示加载结束返回对象的类型
在doInBackground加载时有个返回值,要加载谁就返回谁
把BitmapTask的第三个泛型Void改成Bitmap,表示需要Bitmap对象
doInBackground返回值报错了,改为Bitmap
onPostExecute报错,doInBackground返回值要返回给onPostExecute做界面更新
onPostExecute方法的参数类型改为Bitmap就不报错了
所以BitmapTask第三个泛型和doInBackground返回值类型一致,还和onPostExecute参数类型也一致
getBitmapFromNet传参传到doInBackground了,doInBackground返回结果返回到onPostExecute了
具体怎么返回回去的,已经封装好了,框架中知道怎么用就可以了
有兴趣可以看下它源码,几百行代码很简单,就那一个类和别的类没有任何关联
doInBackground的返回值类型Bitmap可以不用,在doInBackground中返回null,把它写成全局变量
这样onPostExecute也可以拿到全局变量Bitmap,没有必要非得拿doInBackground返回值
面试时可以讲下这三个参数的用法提高亮点
有时为了拿到更好的工资,必须把某些知识点了解的细致,让面试官眼前一亮
3个泛型配好后就可以使用AsyncTask去下载了
doInBackground中参数也传过来了,调用download方法把url传进去去下载,把bitmap返回回去
// 子线程运行
@Override
protected Bitmap doInBackground(Object... params) {
// 开始异步加载
imageView = (ImageView) params[0];
url = (String) params[1];
Bitmap bitmap = download(url);
return bitmap;
}
创建download方法来下载,在这能直接下载是因为doInBackground本来就在子线程,子线程可以直接下载
9.在AsyncTask的doInBackGround使用HttpURLConnection实现下载图片的逻辑
用HttpURLConnection去下载,当然用HttpUtils也可以
new一个URL对象,把url传入,调用openConnection,将返回值强转为HttpURLConnection,叫做conn,tryCatch捕获异常
用conn分别设置请求方式,连接超时时间,读取超时时间,connect开始连接
getResponseCode拿到响应码后判断等于200就去下载,getInputStream拿到输入流InputStream,把它叫做in
BitmapFactory.decodeStream(in)通过输入流生成bitmap对象,把它叫做bitmap
直接通过流生成Bitmap对象,这样省的你去读这个流了,它自己就读好了,读好流后去return把bitmap返回
将HttpURLConnection抽取成全局初始化设置为null,在finally中判断连接是否为null,不为null时disconnect断开连接
//下载图片
public Bitmap download(String url) {
HttpURLConnection conn;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);// 连接超时,没有连接上
conn.setReadTimeout(5000);// 读取超时,连接上了, 但是服务器迟迟不给响应(服务器死循环,一直不给你吐数据)
conn.connect();
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
InputStream in = conn.getInputStream();
// 通过输入流生成bitmap对象
Bitmap bitmap = BitmapFactory.decodeStream(in);
return bitmap;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (conn != null) {
conn.disconnect();
}
}
return bitmap;
}
下载完后一返回bitmap就到doInBackground再次返回到onPostExecute了,这时onPostExecute的参数result就是bitmap对象
onPostExecute中有个super.onPostExecute(result),点击进去发现底层onPostExecute啥都没写,删不删都可以
onPostExecute中去判断,因为有可能会下载失败
如果result不等于null就给imageView把Bitmap塞进去
imageview在doInBackground方法中,需要把ImageView搞成全局变量去访问
onPostExecute中的判断中给imageview去setimageBitmap,把result塞进来
// 主线程运行
@Override
protected void onPostExecute(Bitmap result) {
// 加载完成
if (result != null) {
imageView.setImageBitmap(result);
}
}
通过这些步骤就把网络缓存这块逻辑基本实现了,还需要进行一些细节优化处理
10.通过给imageView去setTag避免ListView因convertView重用导致的item图片相同问题(以前是通过position处理)
onPostExecute这设置图片时要确保此图片就是当前imageview需要的图片
因为图片有重用机制,listview有convertView重用机制,导致多个item可以共用一个iamgeView,造成图片加载错误
有可能下一个item的imageview控件重用的是上一个item的imageview
手机卫士讲过了
imageview去setImageBitmap就把同样的结果result设置为两个不同的imageview对象了
即在NetCacheUtils的onPostExecute的这行代码:
imageView.setImageBitmap(result);
1)给imageview去setTag确保加载的图片是正确的
doInBackground中一加载就把当前imageview和url绑定在一起
就是给imageview去setTag,将url作为参数放在里边,相当于给imageview打了个标记
// 子线程运行
@Override
protected Bitmap doInBackground(Object... params) {
// 开始异步加载
imageView = (ImageView) params[0];
url = (String) params[1];
imageView.setTag(url);// 将imageView和url绑定在一起
Bitmap bitmap = download(url);
return bitmap;
}
2)在onPostExecute的判断中,imageView先getTag拿到对应url
再添加判断this.url是否等于doInBackground的那个url,把doInBackground的url搞成全局
即选中目标url,ctrl+1,选择'Convert local variable to field'
是的话才把它塞给当前imageView
// 主线程运行
@Override
protected void onPostExecute(Bitmap result) {
// 加载完成
if (result != null) {
// 在设置图片之前,确保此图片就是当前imageview需要的图片(因为listview可以重用imageview,导致多个item可能共用一个imageview对象,造成图片加载错误)
String url = (String) imageView.getTag();
if (this.url.equals(url)) {
imageView.setImageBitmap(result);
}
}
}
实现MyBitmapUtils的逻辑总结
1.组图页面PhotoMenuDetailPager的PhotoAdapter中,将BitmapUtils注释掉,换成自己写的MyBitmapUtils
2.PhotoMenuDetailPager的getView中调MyBitmapUtils的display
3.MyBitmapUtils的display中把ivPic,url通过NetCacheUtils的getBitmapFromNet传递给网络缓存NetCacheUtils
4.NetCacheUtils的getBitmapFromNet中new了BitmapTask(继承自AsyncTask)去下载
5.BitmapTask的doInBackground中一旦调用download下载成功
6.就把bitmap塞给了onPostExecute的imageview
运行程序,点击新闻中心侧边栏的组图,图片照样可以出来,和原来使用BitmapUtils加载图片效果一样
网络缓存实现了,可以在NetCacheUtils的onPostExecute中打下日志
System.out.println("从网络加载图片啦...");
NetCacheUtils的onPostExecute中判断url不等于相当于没有给onPostExecute的imageView设置图片
没设置图片就会导致某个item没有图片展示,这个地方其实不用操心,因为用户一上下滑动就会重新调用getView
一调getView就会重新下载一遍,所以肯定能重新加载出来
运行程序,组图页面往下滑动,会不断打印‘从网络加载图片啦...’,每getView一次就会从网上加载一次图片
又继续滑时就会继续加载,因为没有做本地缓存,内存缓存,每次都从网络下载
Day05 06.三级缓存之本地缓存 ##
1.写本地缓存工具类LocalCacheUtils
2.MyBitmapUtils构造方法初始化LocalCacheUtils,抽取成成员变量mLocalUtils
private NetCacheUtils mNetUtils;
private LocalCacheUtils mLocalUtils;
public MyBitmapUtils() {
mLocalUtils = new LocalCacheUtils();
mNetUtils = new NetCacheUtils(mLocalUtils, mMemoryUtils);
}
3.MyBitmapUtils的display中调用getBitmapFromLocal(url)读本地缓存
调用getBitmapFromLocal将url传过去,返回一个Bitmap对象叫做bitmap
// 其次读本地缓存
Bitmap bitmap = mLocalUtils.getBitmapFromLocal(url);
选中MyBitmapUtils的display中调用的getBitmapFromLocal,ctrl+1,选择'Create method 'getBitmapFromLocal(String)'in type 'LocalCacheUtils'',会跳转到LocalCacheUtils并自动创建好了读本地缓存的方法getBitmapFromLocal
4.实现写本地缓存
再创建写本地缓存的setBitmap2Local,需要给它传参数url和Bitmap
1)写本地缓存需要缓存文件夹路径,成员变量处定义路径为LOCAL_CACHE_PATH,等于sd卡绝对路径+文件夹zhxa02_cache
private static final String LOCAL_CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/zhbj_cache";
2)setBitmap2Local中以LOCAL_CACHE_PATH为文件夹,创建文件File
文件以MD5编码后的url命名,资料中将MD5Encoder.java直接拷贝到工具包下
LocalCacheUtils中,在setBitmap2Local中调用MD5Encoder.encode(url)算下MD5,会报错,tryCache下
通过file.getParentFile拿到父文件夹,判断文件的父文件夹是否存在,不存在要重新创建这个文件夹
//写本地缓存
public void setBitmap2Local(String url, Bitmap bitmap) {
try {
File file = new File(LOCAL_CACHE_PATH, MD5Encoder.encode(url));
File parentFile = file.getParentFile();// 获取所在的文件夹
if (!parentFile.exists()) {
parentFile.mkdirs();// 创建文件夹
}
FileOutputStream stream = new FileOutputStream(file);
// 将图片压缩保存在本地, 压缩格式jpeg, 压缩比例100%
bitmap.compress(CompressFormat.JPEG, 100,new FileOutputStream(stream));
} catch (Exception e) {
e.printStackTrace();
}
}
创建文件的mkdirs和mkdir区别
一般都用mkdirs
bitmap有个将图片压缩保存到本地的方法compress
bitmap.compress(format,quality,stream);
参数format: 压缩保存的格式有JPEG,PNG,WEBP格式,一般都保存成JPEG或PNG
WEBP是谷歌出的一款专门针对Web端图片的格式,用JPEG格式比较多
因为JPEG要比PNG小,PNG压缩没有JPEG彻底,所以就用JPEG
参数quality: 压缩质量,写50就压缩50%,写100就是全保帧式压缩,高清无码式压缩,所以就写100
参数stream: 输出流,可以new一个FileOutputStream,把file传过来
压缩格式是jpeg,压缩比例是100%就是不压缩
这样一做,它就已经持久化在本地了
5.实现读本地缓存
获取MD5编码后的url文件名
String fileName = MD5Encoder.encode(url);
将写本地缓存的方法setBitmap2Local中的File代码拷贝过来
然后判断file是否存在
存在就读本地缓存,用BitmapFactory.decodeStream,在它里边new一个FileInputStream把file传进去,返回一个Bitmap,将它return回去
不存在就说明没有缓存,就走到下边的return null了
public Bitmap getBitmapFromLocal(String url) {
try {
String md5 = MD5Encoder.encode(url);
File file = new File(LOCAL_PATH, md5);
if (file.exists()) {
// 有本地缓存
Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
return bitmap;
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
6.MyBitmapUtils的display中调用getBitmapFromLocal(url)读本地缓存时
需要判断本地缓存是否为null,不为null就读到本地缓存了
ivPic.setImageBitmap(bitmap),将本地缓存bitmap传进去填充数据
后边就没有必要从网络缓存去读了,直接return就行了
// 从本地加载图片
bitmap = localUtils.getBitmapFromLocal(url);
if (bitmap != null) {
ivPhoto.setImageBitmap(bitmap);
memUtils.setBitmap2Memory(url, bitmap);// 将图片保存在内存缓存中
return;
}
7.网络缓存NetCacheUtils中图片读取成功后在onPostExecute去写本地缓存
NetCacheUtils中拿到MyBitmapUtils声明的mLocalUtils
MyBitmapUtils中以构造方法形式把mLocalUtils传给NetCacheUtils,省的再创建一个了
MyBitmapUtils构造方法的NetCacheUtils会报错,ctrl+1,‘create constructor 'NetCacheUtils(LocalCacheUtils)'’,跳转到NetCacheUtils并自动创建好了带LocalCacheUtils参数的NetCacheUtils构造方法,接收并抽取成成员变量叫做mLocalUtils
private LocalCacheUtils mLocalUtils;
public NetCacheUtils(LocalCacheUtils localUtils) {
mLocalUtils = localUtils;
}
这时就可以在NetCacheUtils的onPostExecute方法中写本地缓存了
mLocalUtils.setBitmap2Local(url,bitmap);
url还就是我们的这个url,bitmap是我们onPostExecute方法的参数result
// 写本地缓存
mLocalUtils.setBitmap2Local(url, result);
网络数据一加载就写到本地缓存了,下次再进来从本地缓存取数据了,就不用从网络加载
运行程序,点新闻中心,切到组图图片出来了
因为是第一次,首先打印的是‘从网络加载图片啦...’,再往下拉,都是从网络加载图片
因为第一次都要从网络获取图片,然后再往上拉时就打印‘从本地读取图片...’,再往下拉全都是打印‘从本地读取图片...’,因为本地已经有图片了
即使退出程序再进来还是从本地读缓存
本地存在sd卡目录下了,DDMS,mnt/sdcard/zhxa02_cache,
看到它里边有好多MD5命名的文本,把它们导出来后缀名改为jpeg,就是一张一张图片
Day05 07.三级缓存之内存缓存&软引用介绍 ##
内存缓存相当于要将图片保存在内存中
1.写内存缓存工具类MemoryCacheUtils
2.MyBitmapUtils构造方法中初始化MemoryCacheUtils,抽取成成员变量mMemoryUtils
private MemoryCacheUtils mMemoryUtils;
public MyBitmapUtils() {
mMemoryUtils = new MemoryCacheUtils();
}
调用getBitmapFromLocal将url传过去,返回一个Bitmap对象叫做bitmap
// 其次读本地缓存
Bitmap bitmap = mLocalUtils.getBitmapFromLocal(url);
选中MyBitmapUtils的display中调用的getBitmapFromLocal,ctrl+1,选择'Create method 'getBitmapFromLocal(String)'in type 'LocalCacheUtils'',会跳转到LocalCacheUtils并自动创建好了读本地缓存的方法getBitmapFromLocal
3.MyBitmapUtils的display中调用getBitmapFromMemory(url)读内存缓存
将url传过去,返回一个Bitmap对象叫做bitmap
这里读内存缓存和本地缓存都反回了Bitmap,它们用一个Bitmap对象表示即可,将下边读本地缓存的Bitmap删除
// 优先读内存缓存
Bitmap bitmap = mMemoryUtils.getBitmapFromMemory(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
return;
}
MyBitmapUtils的display中调用getBitmapFromMemory方法会报错,自动生成这个方法
4.实现写内存缓存的功能
再创建一个写内存缓存的setBitmap2Memory,需要给它传参数url和Bitmap
内存中是用集合保存图片的
ArrayList存入比较方便,但取时还要知道图片位置,所以选map集合
url为key,bitmap为value就一一对应存起来了
1)MemoryCacheUtils成员变量处new一个HashMap
private HashMap<String,Bitmap> mBitmapMap = new HashMap<String,Bitmap>();
写内存吧缓存的setBitmap2Memory中,mBitmapMap去put,key为url,value是bitmap
//写内存缓存
public void setBitmap2Memory(String url, Bitmap bitmap) {
mBitmapMap.put(url,bitmap);
}
5.实现读内存缓存的功能
读内存缓存的getBitmapFromMemory中直接去get(url)就把bitmap返回回来了,把bitmap直接去return
//读内存缓存
public Bitmap getBitmapFromMemory(String url) {
Bitmap bitmap = mBitmapMap.get(url);
return bitmap;
}
6.MyBitmapUtils的display中调用getBitmapFromMemory(url)读内存缓存时
需要判断内存缓存是否为null,不为null就读到内存缓存了
ivPic.setImageBitmap(bitmap),将内存缓存bitmap传进去填充数据
后边就没有必要从本地缓存去读了,直接return就行了
Bitmap bitmap = null;
// 从内存加载图片
bitmap = memUtils.getBitmapFromMemory(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从内存读取图片...");
return;
}
7.网络缓存NetCacheUtils中图片读取成功后在onPostExecute去写本地缓存
从本地读缓存时要往内存中写,从网络读时也要往内存中写,所以两个地方要写内存缓存
1)MyBitmapUtils读本地缓存时去写内存缓存,这样下次就不用从本地去取
用mMemoryUtils去setBitmap2Memory,把url和bitmap传进去
// 其次读本地缓存
bitmap = mLocalUtils.getBitmapFromLocal(url);
if (bitmap != null) {
ivPic.setImageBitmap(bitmap);
System.out.println("从本地读取图片...");
//写内存缓存
mMemoryUtils.setBitmap2Memory(url, bitmap);
return;
}
接收并抽取成成员变量叫做mLocalUtils
2)NetCacheUtils读网络时也要写内存缓存
NetCacheUtils中拿到MyBitmapUtils声明的mLocalUtils
MyBitmapUtils中以构造方法形式把mMemoryUtils也传给NetCacheUtils
自动修改NetCacheUtils构造方法加上MemoryCacheUtils,接收并抽取成成员变量叫做mMemoryUtils
private LocalCacheUtils mLocalUtils;
private MemoryCacheUtils mMemoryCacheUtils;
public NetCacheUtils(LocalCacheUtils localUtils,
MemoryCacheUtils memoryCacheUtils) {
mLocalUtils = localUtils;
mMemoryCacheUtils = memoryCacheUtils;
}
在NetCacheUtils的onPostExecute的写本地缓存下边去写内存缓存
// 主线程运行
@Override
protected void onPostExecute(Bitmap result) {
// 加载完成
if (result != null) {
// 在设置图片之前,确保此图片就是当前imageview需要的图片(因为listview可以重用imageview,导致多个item可能共用一个imageview对象,造成图片加载错误)
String url = (String) imageView.getTag();
if (this.url.equals(url)) {
imageView.setImageBitmap(result);
// 写本地缓存
mLocalUtils.setBitmap2Local(url, result);
// 写内存缓存
mMemoryCacheUtils.setBitmap2Memory(url, result);
System.out.println("从网络加载图片啦...");
}
}
}
运行程序,一旦从内存取,就在MyBitmapUtils的display方法中打印日志“从内存读取图片...”
点新闻中心侧边栏的组图,第一次进来都要从本地读
(这里第一次进来不是从网络读是因为上边写完本地缓存后,已运行了一次,所以本地缓存已有数据了)
往下拉都是从本地读,再往下拉时都是从内存去读,切换到专题后再切回组图,就会变成从本地读了
因为切换回组图内存被清了一次
8.内存缓存存在内存溢出的风险
内存不够用时会出现内存溢出,上千张图片全塞在MemoryCacheUtils的mBitMap,迟早会堆内存溢出挂掉
总内存哪怕是1G,android虚拟机默认给每个应用分配16MB(或32MB)内存,所以内存溢出和手机总内存无关
java虚拟机有垃圾回收机制,不回收是因为默认是强引用
内存中堆和栈的关系:
堆中保存的是对象,栈中保存的是对象的引用(声明的基本变量,方法名等,栈中放的是小东西
比如声明一个person: person p = new Person();
new Person保存在堆中,p保存在栈中
堆和栈有引用关系,栈中的p指向堆中的Person对象,可以多个引用指向堆中Person一个对象
堆中有很多对象,就有很多引用指向过来,内存溢出是因为堆中对象太多了
垃圾回收器是收垃圾的,不回收不是垃圾的东西
如何判定是垃圾
没人要的东西就是垃圾,堆中的person对象有栈中的p指向引用它,那person就有人要,不是垃圾
堆中某个对象没有栈中的指向引用,就没人要,是垃圾,要回收
也就是垃圾回收器只回收没有引用的对象
MemoryCacheUtils的mBitMap对象会内存溢出
private HashMap<String, Bitmap> mBitmapMap = new HashMap<String, Bitmap>();
因为HashMap集合中这些对象都被HashMap引用进来了
HashMap不死,这些对象就都不死,即使对象再多,垃圾回收器也不会回收
垃圾回收器在内存不足时怎样才能回收有引用的对象
9.java中对象的4种引用级别
强引用 默认引用, 即使内存溢出,也不会回收
软引用 SoftReference, 内存不够时, 会考虑回收
弱引用 WeakReference 内存不够时, 更会考虑回收
虚引用 PhantomReference 内存不够时, 最优先考虑回收!
后三个都对应java的Reference类
右键‘Open Type’,打开Reference.class源码可查看它底层源码
选中Refrence类名,右键‘Type hierarchy of ‘java.lang.ref.Reference’:’
看到PhantomReference(虚引用),SoftReference(软引用),WeakReference(弱引用)都有实现它
默认的强引用没有什么对象对应它
软引用和弱引用用的比较多,虚引用很少用,因为太容易被回收导致内存缓存没有意义
做内存缓存时,Bitmap对象很大,最容易造成内存溢出,用SoftReference把Bitmap对象包装起来,当内存不够时就可以把Bitmap对象回收掉
10.用SoftReference包装Bitmap对象解决内存溢出问题
1)MemoryCacheUtils的mBitmapMap改为:
private HashMap<String, SoftReference<Bitmap>> mBitmapMap = new HashMap<String, SoftReference<Bitmap>>();
2)这是用软引用包装Bitmap,MemoryCacheUtils的getBitmapFromMemory中会报错,原因是:
Bitmap bitmap = mBitmapMap.get(url);
因为get返回的应该是SoftReference对象,所以把这行注释掉,写如下代码:
// Bitmap bitmap = mBitmapMap.get(url);
SoftReference<Bitmap> softReference = mBitmapMap.get(url);
3)mBitmapMap去get(url),返回一个SoftReference<Bitmap>,把它叫做softReference
softReference.get()拿到Bitmap对象叫做bitmap,最后将bitmap返回回去
软引用有可能会被回收,在这判断,如果softReference不等于null才去get获取bitmap
把bitmap返回回去,那把bitmap放在外边,默认置为给null
// 读内存缓存
public Bitmap getBitmapFromMemory(String url) {
//Bitmap bitmap = mBitmapMap.get(url);
Bitmap bitmap = null;
SoftReference<Bitmap> softReference = mBitmapMap.get(url);
if (softReference != null) {
bitmap = softReference.get();
}
return bitmap;
}
4)MemoryCacheUtils中设置图片时,new一个泛型为Bitmap的SoftReference<Bitmap>,把它叫做ref
把ref放在mBitmapMap集合中
//写内存缓存
public void setBitmap2Memory(String url, Bitmap bitmap) {
SoftReference<Bitmap> ref = new SoftReference<Bitmap>(bitmap);
mBitmapMap.put(url, ref);
}
运行程序,使用软引用后没看出啥变化,因为这个图片本来就比较小,但在内存方面已经有了很多长进
万一内存不够,以前会挂掉,现在垃圾回收器会积极回收它
往下滑,打印‘从本地读取图片...’,再往上滑时打印“从内存读取图片...”
从本地获取数据是因为引用已被回收,拿不到Bitmap对象,这时只能从本地加载
Day05 08.三级缓存总结&LruCache ##
1.API版本问题导致使用引用包装Bitmap对象无法处理内存溢出问题
“从Android2.3(API Level 9)开始,垃圾回收器更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠”
因为垃圾回收器更倾向于回收持有软引用或弱引用的对象,即使内存还很够,它也回收
所以内存缓存的利用率很低,因为老从本地去读了
“内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。最核心的类是LruCache(此类在android-support-v4包中提供)
Lru是least recentlly used,即最近最少使用的一种算法
刚访问了a,又访问了b,又访问了c,又访问了a,又访问了d,最近最少使用的是字母b,b对象不常用就可以把它移除掉
LruCache算法原理就是“把最近最少使用的对象在缓存值达到预设定值之前从内存中移除”
2.使用LruCache替换软引用弱引用包装Bitmap对象解决内存溢出问题
MemoryCacheUtils成员变量处注释掉HashMap,声明LruCache,LruCache还是key,value结构
key是String类型的url,value是Bitmap类型
//private HashMap<String, SoftReference<Bitmap>> mBitmapMap = new HashMap<String, SoftReference<Bitmap>>();
// 可以将内存控制在一定范围, 不会超过最大值
private LruCache<String, Bitmap> mLruCache;
MemoryCacheUtils构造方法中初始化LruCache,并计算每个Bitmap图片对象在内存中占用的大小
需要传内存最大值maxSize
手机给每个app分配内存大小默认16MB,给LruCache分配内存时,其他地方也占内存,加载一个activity,切换页面都得耗内存,设置成16MB峰值可以永远控制在16MB内,但跳页面会崩溃
业内统一规范,分配给app的内存最大值maxMemory的1/8,作为LruCache的峰值
maxMemory / 8报错,强转成int类型即可
public MemoryCacheUtils() {
//当前手机给每个app分配的内存大小 模拟器默认16MB
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("max memory:" + maxMemory);
参数maxSize:内存最大值,用总内存的8分之一作为lrucache的最大值
mLruCache = new LruCache<String, Bitmap>((int) maxMemory / 8)){
//计算每个对象的占用内存大小
@Override
protected int sizeOf(String key, Bitmap value) {
//super.sizeOf(key,value);
//value.getByteCount();
int totalCount = value.getRowBytes() * value.getHeight();
return totalCount;
}
};
}
重写sizeOf计算每个对象占用内存的大小
super.sizeOf(key,value)是它自己计算每个Bitmap占用内存值的方法,源码就返回个1,一个图片有时要达到几十K,不可能1字节,这样写计算失误肯定会挂掉
LruCache自己不会计算,开发者要重写sizeOf把每个Bitmap占用内存大小加起来告诉它,才能去计算
value.getByteCount:获取每个Bitmap占用内存的大小和数量
报错:“Call requires API level 12(current min 8):android.graphics.Bitmap#getByteCount”
3.解决getByteCount计算每个Bitmap对象占用内存大小时的版本兼容问题
底层getByteCount源码返回的是int值,即: return getRowBytes() * getHeight();
解决报错:将这行代码复制过来,用int接收totalCount并返回回去
MemoryCacheUtils没有getRowBytes和getHeight方法会报错,这两个方法是Bitmap的方法
所以用参数的Bitmap对象value去调用
高版本才在源码中加了getByteCount方法
低版本没有getByteCount方法,提供的是getRowBytes和getHeight方法,计算后也能实现getByteCount功能,所以用这种方式实现版本兼容(以前遇到版本兼容,貌似都是将版本调到人家需要的版本)
Bitmap.getRowBytes()取每一行字节数 * Bitmap.getHeight()取Bitmap在内存中的高度 = 每个图片占用内存字节数
绘制图片是一个像素点一个像素点密集排列在一起,每个像素点占一个字节,总共是多少个像素就是多少个字节
每一行像素点个数乘以总高度就是所有像素点个数
4.MemoryCacheUtils读内存缓存getBitmapFromMemory时,不在用之前的软引用了,用mLruCache.get(url)返回Bitmap对象bitmap,和HashMap的用法完全一样
//读内存缓存
public Bitmap getBitmapFromMemory(String url) {
// Bitmap bitmap = mBitmapMap.get(url);
//Bitmap bitmap = null;
//SoftReference<Bitmap> softReference = mBitmapMap.get(url);
//if (softReference != null) {
//bitmap = softReference.get();
//}
Bitmap bitmap = mLruCache.get(url);
return bitmap;
}
5.MemoryCacheUtils的setBitmap2Memory中也不在用软引用了,通过mLruCache.put(url, bitmap)
//写内存缓存
public void setBitmap2Memory(String url, Bitmap bitmap) {
//SoftReference<Bitmap> ref = new SoftReference<Bitmap>(bitmap);
//mBitmapMap.put(url, ref);
mLruCache.put(url, bitmap);
}
这就是LruCache在内存缓存中的使用
6.LruCache在内存缓存中的作用
可以将内存控制在一定范围,不会超过最大值
运行程序,切到组图模块往下滑,一直从本地读取图片,再往上滑从内存读取图片,以后再滑就都从内存中取了,以后涉及内存溢出尽量用LruCache处理
7.LruCache原理
源码中总共300多行代码,底层维护了一个LinkeHashMap对象(列表式的HashMap和普通Hashmap没有太大区别)
声明了最大值maxSize是开发者传进来的值,把它设置成了全局变量,new了一个map值初始化
get方法从map中根据参数key拿到Value,put方法给map中放值
只做到这和HashMap没任何区别,LruCache也不会控制大小
trimToSize去裁大小,其实就是控制它的大小
while循环进来,如果当前size小于等于最大值maxSize,说明总大小没有超出最大值,这时break跳出循环不用做任何处理
如果超出了,就从map拿到iterator迭代器中的第一个对象,通过map把对象移除掉后,size为减等于它的大小
源码的SafeSizeOf中已经把sizeOf值减去了一个图片大小,减完之后如果大小还不够
while循环会继续移除对象,就是一个删除操作
每次put时,都会+=safeSizeOf,size每加一个对象size都要加一下,但到最后时每移除一个对象size都应该减一下,通过这个变量控制永远不会超出来
所以你也可以写一个简单的LruCache,就是每次对对象大小判断,超了就删一个对象,没有超就继续添加
8.面试官:如何解决内存溢出?
回答1:
在加载很多图片时会出现内存溢出,一般情况下是不太会出现内存溢出的
那么如果真的加载很多图片,可以用三级缓存防止内存溢出
分别是内存缓存,本地缓存和网络缓存,优先从内存缓存加载图片,其次再从本地缓存,最后才从网络缓存下载
这样既能提高获取图片的速度,也能够减少流量的消耗
本地缓存和网络缓存没啥可说的,主要是内存缓存要控制好,控制不好绝对会内存溢出
因为需要将图片放在HashMap集合中,内存越来越大肯定会挂掉,因为java虚拟机以及android中的dalvik虚拟机只给每一款应用默认分配16MB内存空间,所以很容易溢出
所以可以用谷歌V4包提供的LruCache,它可以将内存控制在一定范围内,超出就自动把那些对象移除掉来防止内存溢出
回答2:
逼格更高回答,讲LruCache时这样说,关于内存溢出确实把我搞的头疼,最开始写了一个HashMap,然后直接保存key,value,当时就觉得可能有点问题,然后我就滑滑滑,图片少还行,但图片一多就直接挂掉了
我一想就知道,这个肯定是HashMap太大了,后来就去网上查,问怎么能把对象及时回收掉,网上解释说,这个之所以不回收是因为HashMap都是强引用,会导致对象不被回收,即使垃圾回收再频繁也不回收
有个解决方案叫用软引用或弱引用可以让垃圾回收器在内存不够时,偏向于回收这个对象,然后我就用软引用把Bitmap图片对象包装了一下,后来看起来效果还不错
但是后来发现在低版本上看起来还可以,但在2.3版本以上总是从本地缓存去加载图片,不再从内存缓存去加载图片,感觉内存缓存没起多大作用
嘴周查到了一篇博客介绍说:“谷歌官方文档说Android2.3(API Level9)之后就不再推荐使用软引用弱引用了来处理内存溢出问题,建议用LruCache,我就用LruCache改造了一下,后来发现可以
后来我就闲的没事,看了一下LruCache源码,也就几百行代码,里边就封装了一个HashMap,每次put,get时,控制一下它的大小,不要超出,超出就删除,我自己写都能写出来一个
面试官就喜欢听你这些细节,细节说的越细就觉得很真实,而且会觉得你很喜欢动脑子研究,对项目代码优化有一种热情
一上来就LruCache,很平淡,你就循循善诱把他搞的越来越嗨,他就觉得你比较牛逼
内存溢出有很多方面,现在学的是图片缓存,所以切入点尽量从图片去说,实际项目中百分之八十的内存溢出都是由图片造成
百分之二十是由于你写的代码太垃圾,一个person对象就搞出内存溢出了
说明你是哪里搞了一个死循环或哪里变量集合太大了,成千上万的对象在那个集合中
9.在代码中改用MyBitmapUtils
BitmapUtils写完了,目前是用它加载组图图片,TabDetailPager中也有BitmapUtils,getView时也用到BitmapUtils,全都改用MyBitmapUtils
TabDetailPager的NewsAdapter的局部变量处,注释掉原来的BitmapUtils,声明MyBitmapUtils
//private BitmapUtils mBitmapUtils;
private MyBitmapUtils mBitmapUtils;
NewsAdapter构造方法中注释掉原来初始化BitmapUtils的代码,然后初始化MyBitmapUtils
public NewsAdapter() {
//mBitmapUtils = new BitmapUtils(mActivity);
//mBitmapUtils.configDefaultLoadingImage(R.drawable.news_pic_default);// 设置默认加载中的图片
mBitmapUtils = new MyBitmapUtils();
}
这样别的方法都不用变,getView的disPlay方法MyBitmapUtils中也叫disPlay,不会变
运行程序,发现没有问题,使劲滑快速切换页面,终于崩溃了,错误日志error原因是“Out of memory on a 230412-byte allocation”,意思是内存溢出
10.已经实现了MyBitmapUtils三级缓存还会内存溢出原因
已经写了MyBitmapUtils三级缓存了还会内存溢出,因为图片加载过程中,现在写的三级缓存很肤浅
图片方面要做到完美还有很多工作,比如对图片压缩,不损失清晰度前提下压的更小一些,内存占的更少一些
这个挂掉有可能是因为垃圾回收器回收的不及时,也会造成内存溢出
看BitmapUtils怎么去做的,再切过来,把上边TabDetailPager中NewsAdapter局部变量处被刚才注释掉的BitmapUtils放开,把MyBitmapUtils注释掉
把NewsAdapter构造方法中初始化BitmapUtils的代码也放开,把初始化MyBitmapUtils的代码注释掉
点display进到源码,Xutis关于图片加载的BitmapUtils写了很多类
XUtilsLibrary中bitmap包下有很多类都用来处理图片加载
下边还有回调包,核心类包,factory包,仅BitmapUtils就写了这么多类
而MyBitmapUtils只写了四个类而且代码很少
MyBitmapUtils的display和源码中的display思路一样,只不过没有做很多细节处理
源码display中先调getBitmapFromMemCache从内存缓存取图片,跟踪判断处的mMemoryCache发现是LruMemoryCache(在成员变量处声明的),LruMemoryCache就是做了一些修改的LruCache
如果内存缓存没有,就用BitmapLoadTask这样一个异步AsyncTask,继承的是PriorityAsyncTask,优化了很多东西
MyBitmapUtils的缓存是在主线程去缓存的,即MyBitmapUtils的display处理本地缓存时在主线程中去getBitmapFromLocal,这个不好,图片如果比较大会造成主线程稍微阻塞
所以BitmapUtils源码中是放在异步中的,PriorityAsyncTask中调用了getBitmapFromDiskCache做了磁盘缓存,把本地缓存放在了异步,先从磁盘去读,读不到再去downloadBitmap下载,细节做了很多优化处理
实际开发中用XUtils的BitmapUtils,不要用自己封装的MyBitmapUtils
封装MyBitmapUtils是为了面试增加亮点
加载图片的第三方框架:BitmapUtils,imageDownloader(轻量级,只有几个类),谷歌自带的BitmapFun(核心类ImageFetcher),都了解下
Day05 09.屏幕适配 图片适配 ##
1.图片适配
开启4种分辨率的模拟器
drawable多个目录下放命名相同内容不同的图片
运行程序,查看在不同模拟器上的显示效果
做法: 美工只做一套1280*720图片,放置在drawable-xhdpi目录
2.布局适配
针对特定分辨率,创建layout文件夹: layout-800x480, layout-land(表示横屏)
800x480 和其他分辨率模拟器对比
该方式不到万不得已,一般不用
3.尺寸(dimens)适配
设备密度: float density = getResources().getDisplayMetrics().density;
dp = px / 设备密度
常规设备密度: 320x240(0.75), 480x320(1), 800x480(1.5), 1280x720(2)
创建文件夹values-1280x720, 在dimens.xml中制定尺寸, 适配屏幕
做法: 此方法比布局适配更常用. 美工提供像素px值, 我们使用前需要用px除以设备密度,转换成dp后,写在布局文件中
案例分析: 智慧北京新手引导页小圆点处理
4.权重适配
android:weightSum="3" //表示总权重数
做法: 当布局有严格比例分配时, 可以使用权重来处理
5.代码适配
int width = getWindowManager().getDefaultDisplay().getWidth();
int height = getWindowManager().getDefaultDisplay().getHeight();
tv1.setLayoutParams(new LayoutParams((int)(width0.5), (int)(height0.2)));
做法: 如果是自定义控件, 没有使用xml布局文件时, 可以在代码中动态设置宽高
案例分析: 智慧北京侧边栏宽度处理
现在ios都开始强调屏幕适配了,ios屏幕适配很简单,4s,5,6,6plus数的过来
android有人统计过有上万个屏幕尺寸,那怎么去适配
其实android屏幕适配没那么费劲,都比较简单,分5种屏幕适配方式
图片适配
图片适配是放在不同图片文件夹中进行适配
new一个demo名称为ScreenAdapterPic,activity_main.xml布局中写一个ImageView
找一张美女图片p_8.jpg改名为image.jpg,放在drawable-hdpi目录
给ImageView添加属性:android:src="@drawable/image"
运行320480模拟器,480800模拟器,240*320模拟器
在三个模拟器上运行美女图片展现效果不错
所以android屏幕没那么复杂,自己会按照屏幕去适配
480800屏幕上的图片放大导致有点虚,需要换一张图片,在drawable-hdpi中换会导致320480屏幕也换
为了保证320480,240320在不变的情况下,让480*800换成另一张图片
要用到drawable-hdpi,drawable-ldpi,drawable-mdpi,drawable-xhdpi,drawable-xxhdpi文件夹
平时放图片时,全都放在了drawable-hdpi目录下,它是高分辨率
5种主流分辨率
hdpi--480*800是高分辨率 设备密度1.5
mdpi--320*480是中分辨率 设备密度 1
ldpi--320*240是低分辨率 设备密度0.75
xhdpi--1280*720是超高分辨率 设备密度2
xxhdpi--1920*1080 设备密度3
上边5种分辨率是一个涵盖范围,480800是hdpi,480854也是hdpi
非主流会基于5个主流分辨率某个阶段的分辨率做或大或小调整,但还在某个区域浮动,所以5个分辨率文件夹是5个主流范围
一般情况,分辨率越大,设备密度越高
希望在480800屏幕上展示另外一张图片很简单,找到需要展示的图片p_2.jpg还是改名为image.jpg,480800对应的是drawable-hdpi文件夹
还需要在240320屏幕上展示另外一张图片,再找一张图片,240320对应的是drawable-ldpi,在ldpi目录下再放一张图片p_3.jpg继续重命名为image.jpg,都要叫image,因为布局文件activity_main.xml中src找的就是image
分别在3个模拟器运行,240*320模拟器展示的是足球小妹
320*480展示的是白衣小妹
480*800模拟器展示的是光腿小妹
发现项目在不同分辨率模拟器运行,展示的是不一样的图片
activity_main.xml一个布局,在不同图片目录下放名称相同内容不同的图片,导致每个屏幕展示的不一样
图片适配特点
更优先去找屏幕分辨率对应的文件夹下的图片,比如320*480模拟器会先找drawable-mdpi中的图片,如果找到了就直接展现在屏幕上,找不着再找别的文件夹
之前把图片都放在了drawable-hdpi目录下,但实际运行时,先找它自己所对应的文件夹,没有找到才找别的相近的文件夹
这就实现了屏幕适配,在不同尺寸的屏幕上,不同分辨率的屏幕上能加载不一样的图片,这就是图片适配
Day05 10.布局适配 不常用 ##
1.布局适配:针对个别分别率的屏幕单独写一个布局
new一个demo名称为ScreenAdapterLayout
activity_main.xml中添加text为helloword的textview
在不用分辨率屏幕的模拟器上运行,不同屏幕上肯定都是helloword
有时希望在320480上是一种布局,480800上是另一种布局
专门针对480*800适配一套新布局
布局也可以放在不同文件夹中,新建文件夹layout-800x480
平时项目布局目录很少,但androidSdk的platforms/android-18/data/res/中有很多布局目录适配各种情况
除了适配尺寸,还可以适配语言,drawable-en-hdpi的en指英文
layout-sw600dp,layout-sw720dp是适配不确定分辨率的屏幕,sw指superwidth,超级宽的600的屏幕,这是一个大分辨率
还有layout-xlarge,超大分辨率
此项目写的layout-800x480文件夹只直接针对它的分辨率去适配,将layout文件夹下的布局文件activity_main.xml抄一份放在layout-800x480文件夹,可以在里边完全写另一套布局,把TextView改成Button都行
320*480模拟器上运行,展示的是TextView的helloword文字
480*800模拟器上运行,展示的是Button的helloword文字,完全是另外一套布局了,代码还是一套代码,只不过写了两套布局跑在了不同分辨率模拟器上
2.布局适配方式
比如是480*800,大分辨率800写在前边,小分辨率480写在后边,中间小写的x表示乘号,即:layout-800x480
现在有两套布局,MainActivity去findViewById应该拿TextView还是button
把TextView改成Button情况比较极端,没有什么需求让两个不同屏幕展示不同页面,一般不会两个分辨率布局完全不一样,只会修改布局间距之类让页面好看点,保证MainActivity在findViewById时不会有任何问题
3.有些需求用dp无法适配的很好,所以需要用布局适配
小屏幕上展示的很匀称标准,但在大分辨上时,展现的一点也不匀称标准,这时再没有其他方法解决
之所以展现的不匀称标准是因为页面控件之间的margin写的比较小了,包括控件宽高写的有点小了
那有哥们儿用的是dp,dp可以根据设备去变
当然要用dp,但是dp要根据设备的屏幕去变,能力有限,在非常高分辨率屏幕上,dp只是变大一点,但整个屏幕范围内,变化太小,导致控件看起来小,所以需要对尺寸重新定义,单独针对这个分辨率写一套布局文件,把宽高,margin重新写一下,这就是布局适配的用处
Day05 11.尺寸适配 常用 ##
ScreenAdapterDimen的demo中,activity_main.xml中用线性布局作为根布局,方向vertical,TextView宽填充屏幕,高50dp,background背景色为#f00红色,把文字删掉,就要这样一个界面,预览效果是红色长方形
TextView复制一份,background背景色改为#0f0绿色,宽度设置成红色Textview一半,期望是一半的话,可以写死,320*480模拟器上屏幕宽度320像素,实际写一半是160像素,因为设备密度是1,在布局文件中写时,第二个绿色TextView宽度直接写成160dp,它对于设备密度是1的情况下,一半,对比下来就是160相当于160像素
运行项目在320*480的模拟器上,比较完美
运行在240*320的模拟器上,也是完美的
运行在480800的模拟器上,明明是针对320480,用一半160像素算的,在不同分辨率的模拟器上都显示的一半
这就是因为dp起了很大作用,如果当初写成像素,绝对会挂掉,我们写的是dp
dp:设备适配的分辨率,所以尽量用dp,不要用像素
dp= px/ 设备密度
出现这种情况是因为每个屏幕设备密度都不一样,MainActivity的onCreate中分别打印设备密度:
//设备密度
float density = getResources().getDisplayMetrics().density;
System.out.println("设备密度:"+ density);
运行在320*480模拟器上,打印的设备密度是1.0
屏幕宽度320像素的一半是160像素
运行在240*320模拟器上,打印的设备密度是0.75,160dp乘以0.75等于120像素,屏幕宽度240像素的一半是120像素,所以也刚好是一半
运行在480*800模拟器上,打印的设备密度是1.5,1.5乘以160dp等于240像素,屏幕宽度是480像素的一半是240像素,也刚好是一半
这就是设备密度的好处
现在有一个小问题
运行在1280*720模拟器上,这个分辨率太大,竖着根本展示不下只能横着, 看到现在就有问题了,完全不是想要的样式
1280720设备密度是2.0,2.0160dp等于320像素,但320像素不够1280像素的一半,有人说这个是横屏所以会出错,其实竖屏也有问题,竖屏时模拟器宽度是720像素,720像素的一半是360像素,算出来才是320像素
非得在1280*720的模拟器上也展示一半,有人说还不如用权重,这个线性布局你一半我一半,把右边的一半隐藏掉就行
这个待会儿再讲,现在就想用dp值实现
activity_main.xml的第二个textview的宽度值不能写160dp
1280*720模拟器宽度是1280像素,1280的一半是640像素,640像素除以设备密度2等于320dp
把它的宽度由原来160dp改成320dp,其他屏幕的模拟器就有问题了
为了不影响别的屏幕的情况下还能修改1280*720的分辨率,res/values/dimens.xml,dimens以前用的不多,在屏幕适配时就可以用了,在dimens.xml中添加新尺寸,默认宽度是160dp,即:
<dimen name="width">160dp</dimen>
再回到activity_main.xml,就可以不把第二个TextView宽度写死,写成:
android:layout_width="@dimen/width"
在1280*720屏幕上展现另外一个dimen,原来把图片放在别的文件夹下适配图片,布局放在别的文件夹下
values也可以新建一个values-1280x720放在values文件夹下,它适配的是1280x720屏幕
把values文件夹下的dimens.xml拷贝到values-1280x720文件夹下,在这个dimens.xml中把宽度写成320dp
<dimen name="width">320dp</dimen>
values-1280x720文件夹下的dimens.xml中只需要写需要的这个width值就可以了,可以把其他不需要适配的dimen标签删掉
运行在1280*720模拟器上,展示的刚好是一半了,因为它优先去找values-1280x720文件夹下的dimens.xml,宽度读取的就是320dp
其他比如480*800读的是values文件夹下的dimens.xml中默认为160dp的width
为什么尺寸适配比较常用,布局适配不常用?
分析下刚才案例:
小屏幕上展示的布局很均匀,但在大屏幕上出现不均匀效果,完全没必要写一套布局文件去布局适配
因为主要是尺寸问题,比如宽,高,margin问题
可以用尺寸适配,让布局文件从dimens.xml去读尺寸,根据分辨率需求,在1280*720屏幕上展示时,单独写文件夹values-1280x720,将dimen重新声明一下,重新声明宽度,高度,margin都很大的这样一个dimen,就能够达到适配目的
所以有了尺寸适配后,布局适配就可以废弃了
Day05 12.权重适配 ##
权重适配只适用于Linearlayout,线性布局才有权重
ScreenAdapterWeight的demo的activity_main.xml中根布局为线性布局,方向为Vertical,再写个LinearLayout,方向为horizontal,第二个LinearLayout中添加TextView,背景background为#f00红色,预览到它是红色长方形
具体布局文件如下:
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
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:orientation="horizontal"
android:weightSum="3">
<TextView
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="#f00" />
<TextView
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:background="#0f0" />
</LinearLayout>
</LinearLayout>
添加第二个TextView,背景background为#0f0绿色
红色和绿色各占一半,设置weight都为1就行,在任何屏幕上都是你一半我一半
权重适配挺好用,但有局限性,只能用线性布局,要么水平方向,要么上下方向
需求:希望红和绿分别占1/3,留1/3空白
有人说再添加一个TextView,背景background为蓝色#00f,visibility为gone,但这样就没有1/3空白了,gone后不占位置,设置成invisible占位就可以实现这个效果
还有一种方式实现这个效果
给线性布局添加 android:weightSum="3",然后将两个TextView宽度的match_parent都改成0dp,这样红色1/3,绿色1/3,剩下的1/3就是空白
Day05 13.代码适配 ##
其他适配没法解决时用代码适配
ScreenAdpterCode的demo的activity_main.xml中写两个TextView,宽高都包裹内容,即:
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#f00" />
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0f0" />
</LinearLayout>
需求:下边textview宽度是屏幕的一半
在第二个textview中可以写dp,宽度,权重之类去实现也可以
这里用代码给它设置宽高,第二个textview加id为textview
用代码计算出屏幕宽高直接显示上来,MainActivity去findViewById
TextView textView = (TextView) findViewById(R.id.textview);
LayoutParams布局参数可以设置宽高,它爹是在activity_main.xml中的线性布局LinearLayout
LayoutParams layoutParams = textView.getLayoutParams();
写一个LinearLayout.LayoutParams,强转成LinearLayout.LayoutParams,即:
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView.getLayoutParams();
修改高为屏幕高度的1/5,宽为屏幕宽度的1/2:
通过WindowManager拿到屏幕宽度和高度
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
高度写成屏幕高度乘以0.2,1/5高度,强转成整数,返回用params.height接收
宽度params.width等于屏幕一半
params.height = (int) (height * 0.2);
params.width = width / 2;
最后把参数重新设置给textview,即: textView.setLayoutParams(params);
完整代码如下:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
TextView textView = (TextView) findViewById(R.id.textview);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) textView
.getLayoutParams();
params.height = (int) (height * 0.2);
params.width = width / 2;
textView.setLayoutParams(params);
}
}
通过动态修改布局参数宽高去修改textview的宽高,这就是代码适配
运行程序,320*480模拟器根据当前屏幕宽高去计算,这就是代码适配,通过代码方式动态设置宽高
Day05 14.智慧西安屏幕适配 ##
1.使用dp2px适配导航页小圆点间距问题
智慧西安项目分别运行在480800模拟器,320480模拟器
320*480模拟器上,新手引导页小圆点间距大概能再塞一个小圆点
480*800模拟器上,塞不下一个小圆点,说明间距变小了
1280*720模拟器上,小圆点间距更小,因为设置小圆点间距时写的是像素
zhxa02项目的GuideActivity中,第76行leftMargin写的是10,意思是10像素,10像素对于320480还挺多,它宽度整个才320像素,但10像素对480800屏幕就稍微小一些了,所以会发现间距要小一些
用dp解决问题
把dp改成像素后再塞给leftMargin
GuideActivity第76行的params.leftMargin = 10 是10像素,可以用dp值转成px后再塞给它
直接拷贝之前写的DensityUtils,即:
DensityUtils.java
public class DensityUtils {
public static int dp2px(float dp, Context ctx) {
float density = ctx.getResources().getDisplayMetrics().density;
int px = (int) (dp * density + 0.5f);
return px;
}
public static float px2dp(int px, Context ctx) {
float density = ctx.getResources().getDisplayMetrics().density;
float dp = px / density;
return dp;
}
}
dp转成px应该用dp值乘以设备(像素)密度,加0.5f意思是四舍五入
int px = (int) (dp * density + 0.5f);
给两个方法都添加static,方便调用
将GuideActivity第76行原来代码注释掉,用dp2px,参数dp为10dp,即:
// params.leftMargin = 10;// 从第二个圆点开始设置左边距
params.leftMargin = DensityUtils.dp2px(10, this);
这样设置的是10dp而不是10px
设备密度为1的320*480模拟器上是10px,但在别的分辨率屏幕上时,就不是10px了
会根据设备密度动态变化达到屏幕适配
480*800的模拟器上,间距就变的很大了,中间能塞一个小圆点大小,这个就是屏幕适配具体在zhxa02项目的应用
2.使用代码适配适配侧边栏宽度
320*480模拟器上,侧边栏不太宽
480*800模拟器上,侧边栏太宽
更高分辨率屏幕上更宽
当时是在MainActivity第36行代码设置的宽度:
slidingMenu.setBehindOffset(200);// 预留200px宽度, 200/320 = 20/32 = 0.625
预留了200px宽度,200px对320480挺多,但是480800挺少
之前预留的200px是基于宽度为320(320*480)模拟器,是屏幕宽度的0.625倍,将那行代码注释,应该根据屏幕宽度动态预留一定像素值
侧边栏宽度的代码适配,具体代码如下:
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
int screenWidth = wm.getDefaultDisplay().getWidth();// 先获取屏幕宽度
SlidingMenu slidingMenu = getSlidingMenu();
// slidingMenu.setBehindOffset(200);// 预留200px宽度, 200/320 = 20/32 =
// 0.625
slidingMenu.setBehindOffset((int) (screenWidth * 0.625 + 0.5));
480*800模拟器上,看到没有以前那么宽
当然别的屏幕也不会受影响
320*480模拟器上,效果没问题
现在是按照屏幕有多宽,按百分比去展现
Day05 15.屏幕适配总结 ##
屏幕适配面试回答
写代码时养成良好的屏幕适配习惯:
多用dp不用px,多用线性布局和相对布局不用绝对布局(谷歌把它废弃了),如果必须传px,将dp转为px后再使用
1280*720主流屏幕开发的,后期在其他分辨率屏幕上运行可能会出现布局不协调或显示不全
这时候就需要图片适配,尺寸适配,布局适配,权重适配,代码适配
图片(drawalbe-xxxx),布局(layout-xxx x xxx),尺寸(values-xxxx x xxx中的dimens中添加尺寸)
Day05 16.极光推送的使用 ##
主动拉取:需要数据时主动调接口从服务器获取数据
推送是属于被动的
服务器发广告或通知,手机被动接收到广告或通知消息显示出来
聊天功能中被动接收到的消息也是推送技术实现的
有很多专门做推送功能的第三方公司,比如极光推送,百度云推送,个推,我们只需要接入第三方平台,就可以快速实现需要的推送功能,智慧西安项目用极光推送实现推送功能
1.zhxa02项目对应的极光推送demo的下载使用
官方网址: https://www.jpush.cn/
在极光推送平台注册账号,在控制台中注册需要实现推送功能的app,平台会生成一个appkey作为应用的标识进行推送
在极光推送平台的android端注册需要实现推送功能的app为zhxa02,上传应用图标,指定应用包名为com.itcast02.push(与清单文件中包名保持一致,项目结构包名com.exampe.jpushdemo只是结构而已),点击“创建我的应用”在生成的应用信息中分配了一个appkey需要保存下来
下边有个快速集成的入口,点击“下载Android Example"下载并运行zhxa02的例子程序push-example,清单文件中把权限和其它的都配好了,包括appkey
libs文件夹下arm包(arm-64-v8a,armeabi-v7a)是.so文件
后边会讲到jni,其实是c代码写完后打成可执行文件.so文件,让android能够去运行
处理器是arm64-v8a,armabi,armeabi-v7a处理器,目前使用的模拟器是x86的,cpu不兼容,需要启动一个arm的模拟器去运行项目
把apk发给你们,你们在真机上装一下,我在极光推送后台发消息,你们接收,有些同学可能没收到,成功率在90%以上,失败的可能是手机对后台服务进行了拦截,不让后台服务运行,比如小米有些机型
apk界面的“高级功能”可以设置标签(Tag)或别名(Alias),我刚才是广播给所有人,也可以输入要给广播的设备标签广播给个别人
官网后台没出现别名或标签,可尝试再运行下程序,那这时给标签为"呵呵"的发送,就只有它收到了
给项目中集成极光推送时,可以完全仿照demo(push-example)实现
Day05 17.推送sdk使用 ##
官网新建应用zhxapushdemo,包名com.itcast02.pushdemo, 注册什么包名,实际应用清单文件的包名就得是什么,会对包名有验证
eclipse新建项目pushdemo
下载android端的开发文档及sdk
"Jpush-Android-sdk-1.8.0.zip" 是sdk
"Android SDK API- 极光推送.html" 是开发文档
用浏览器打开开发文档
前边是功能说明,点击里边的‘Android SDK 集成指南’,查看SDK集成步骤
1.解压缩 jpush-android-release-2.x.y.zip 集成压缩包(sdk)
复制libs下的jar包到自己项目pushdemo的libs目录下
复制 res/ 中drawable-hdpi, layout, values文件夹中的资源文件到自己项目的 res/ 对应目录下
拷贝sdk中的AndroidManifest.xml文件中相应的配置到自己项目的AndroidMainfest.xml中
自定义MyReceiver继承BroadcastReceiver
添加代码,init初始化SDK,我们需要调一下init方法,然后再去setDebugMode设置为调试模式,那具体怎么去添加呢?它说init方法只需要在应用程序启动的时候调用一次该API就可以了,下边举例中写的是一个自定义的ExampleApplication,在这个自定义的ExampleApplication中是不是调用:
JPushInterface.setDebugMode(true);
JPushInterface.init(this);
把这个JPush给初始化了一下,所以在这个项目中,我们自己写一个自定义的MyApplication,在这里边是不是还可以捕获全局的异常,做过吧,将上边初始化JPush的代码拷贝到我们的MyApplication中即可
当然,MyApplication也需要到清单文件中去注册一下
即我们按照“Android SDK API- 极光推送.html”文档一步一步去做,就可以将极光推送集成到我们的项目中
集成进去之后,就可以去运行项目了,但运行的话,我们将来万一收到消息之后,我们如何在客户端上去收到这个服务器的消息呢?
5.那这时候就需要我们刚刚写的MyReceiver来接收消息了,也就说极光推送它是通过这个自定义的Receiver来去接收所有的消息的,那我们看下这个Receiver到底怎么去接收呢?
这时候我们可以去看下“Android SDK API - 极光文档.html”,这个是比较高级的一个文档,在这里边我们看下Receiver是在哪里去做简绍的,然后按照他的提示一步一步往下做就可以,我们把代码示例的onReceive中的代码全部拷贝到咱们自己写的MyRecceiver的onReceive方法中
这时候我们需要写一个接收消息的页面TestActivity,如果不需要跳的话,那就在MyReceiver中把这个跳TestActivity的代码注释掉
6.运行程序,并在极光推送后台发送消息,看能不能走到MyReceive的Action中来,一旦收到通知,他是不是就会打印MyReceiver.java中的消息日志,是不是打印了“收到通知”的日志,同时通知是不是在通知栏里边啊,而且这个通知是不是还可以被点击啊,一点的话,是不是还会打印“用户点击打开了通知吧”
那其实有时候点击打开这个通知的时候,我们是希望能够从这个通知栏里边去获取一些信息,比如一点之后,要跳哪个网络链接啊,或者加载哪个接口啊对吧,我们需要拿到这些信息,那这些信息怎么去拿呢?
其实在文档里边也有简绍,比如说你想拿到通知的标题的信息,那这时候从Bundle里边可以拿到他的title哈,这是他的标题,我们可以在MyReceiver.java中打印一下,也可以把EXTRA_ALERT打印下,这个是推送的具体的内容, 还有一个EXTRA_EXTRA,这个指的是我们推送下来的附加字段,如果你觉得一个通知的标题和通知的内容已经不足以包含你的信息的话,我们可以拿到他附加的一个信息,我们也打印下
我们再运行下程序,看能不能把我们通知的具体信息都给我打印出来,是不是都打印出来了
注意:
自定义的MyReceiver将来是做什么用的呢?就是用来接收我们广播的,就是我们的广播接收器
那这个就是我们极光推送sdk的简单使用,下去之后呢,你们可以回去在宿舍有网的情况下可以去体验一下这个推送怎么去玩,这就是我们的这样一个推送,我们先简绍到这里
Day05 18.总结 ##
所以你会发现推送的话,它核心就是一个Receiver,在Receiver中我们接收各种各样的消息就可以了
然后呢,我们把今天内容总结下:
今天最核心的是讲我们的三级缓存和屏幕适配,那剩下的需要了解的,其实就是这个推送,我们了解下就行了,那以后你的项目中要用的话,其实可以接入第3方的平台,然后实现一个推送的效果,那关于推送,其实我们今天只是讲了一部分,明天会把推送的一些原理,它的底层的实现再去简单的简绍下