最近一段时间,Android行业大不如从前轻松,企业要求越来越高了,就算入职了很多时候Android这块也不太受重视,现在Android开发者还能经常接到新需求就算是很幸运的事了,我最近的工作也是没什么活,闲来无事就整理一下Android的一些东西,以免遗忘。
这次要做的类似于许多新闻资讯类app,底部tab切换不同的功能页面,首页中又可以切换不同的新闻类型页面,当切换到最后一个新闻fragment时,再向后滑则父viewpager滑动,具体实现是用的ViewPager+Fragment+ViewPager+Fragment,tab用的是desgin库中的TabLayout,fragment实现懒加载,数据源用的是聚合数据(https://www.juhe.cn/),网络框架用的是张鸿洋大神的OkhttpUtils,因为只是做一个效果,就不搭建Rxjava+Retrofit网络请求框架了,主要用到的三方依赖如下:
compile "com.android.support:design:25.+"
compile 'com.google.code.gson:gson:2.8.2'
compile 'com.zhy:okhttputils:2.6.2'
compile 'com.android.support:recyclerview-v7:25+'
一、准备工作
1. BaseApplica
可以自定义一个Application,一是为了初始化OkhttpUtils,另一个是为了获取一个使用较为方便的Context,因为fragment嵌套过多,使得Context获取比较麻烦
public class BaseApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
// .addInterceptor(new LoggerInterceptor("TAG"))
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
//其他配置
.build();
OkHttpUtils.initClient(okHttpClient);
}
public static Context getContext(){
return context;
}
}
2. SharedPreferencesHelper
定义一个SharedPreferences用户偏好存储主要是因为我想实现一个可以添减新闻类型的效果,不过因为昨晚感觉时间有点晚了,而且代码量对博客而言已经不少了,就给这个思路,有兴趣或者有这个需求的兄弟可以参考下这个思路完成。
public class SharedPreferencesHelper {
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
public SharedPreferencesHelper(Context context,String preferenceName){
sharedPreferences = context.getSharedPreferences(preferenceName,Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}
public void setSharedPreferencesString(String tag,boolean isFirst) {
editor.putBoolean(tag,isFirst);
editor.commit();
}
public boolean getSharedPreferencesString(String tag) {
return sharedPreferences.getBoolean(tag,false);
}
public void setSharedPreferencesList(String tag, List list){
if (null==list||list.size()==0) {
return;
}
Gson gson = new Gson();
String s = gson.toJson(list);
editor.clear();
editor.putString(tag,s);
editor.commit();
}
public List getSharedPreferencesList(String tag) {
List datas = new ArrayList<>();
String string = sharedPreferences.getString(tag, null);
if (null==string) {
return datas;
}
Gson gson = new Gson();
datas = gson.fromJson(string,new TypeToken>(){}.getType());
return datas;
}
}
这个类很简单,初始化的时候传入Context和存储名,创建sharedPreferences和Editor,然后存取文字一对方法,存取list集合一对方法,因为SharedPreferences存储只能存一些基本类型,所以先用Gson将list转化为String类型再进行存储,取的时候再用Gson把数据转化为list。使用也很简单,在MainActivity中进行是否初次进入判断,初次进入赋给喜欢新闻类型的list集合初始值,当然一般app的设计都是有几个新闻类型是一定存在的,这个无所谓,实现上大同小异。然后在初始化浏览新闻页面时,获取新闻类型集合,根据集合的数量创建相应个数的fragment,每个fragment根据新闻类型再进行网络访问获取新闻内容,具体实现如下。
二、页面设计
1. MainActivity
MainActivity是承载整个ViewPager+Fragment+ViewPager+Fragment的载体,专业来说是他们的父布局,我对它的设计很简单,就是上边一个ViewPager滑动页卡,下边一个TabLayout存放tab,然后在viewPager中添加fragment,TabLayout中添加tab,最后将二者关联起来:
public class MainActivity extends AppCompatActivity {
private TabLayout tabLayout;
private ViewPager viewPager;
private List fragments = new ArrayList<>();
private List pageTitle = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferencesHelper isFirstSave = new SharedPreferencesHelper(this, "isFirstSave");
if (!isFirstSave.getSharedPreferencesString("isFirst")) {
isFirstSave.setSharedPreferencesString("isFirst",true);
List list = new ArrayList<>();
list.add("头条");
list.add("社会");
list.add("科技");
new SharedPreferencesHelper(BaseApplication.getContext(),"newsDatasSave").setSharedPreferencesList("newsDatas",list);
}
viewPager = (ViewPager) findViewById(R.id.activity_main_pager);
tabLayout = (TabLayout) findViewById(R.id.activity_main_tab);
initPager();
initTab();
tabLayout.setupWithViewPager(viewPager);//关联viewPager和fragment
for (int i = 0; i < fragments.size(); i++) {
tabLayout.getTabAt(i).setText(pageTitle.get(i)).setIcon(R.mipmap.ic_launcher);//设置tab显示文字和图片
}
}
private void initPager() {
fragments.add(new MainFragment());
fragments.add(new MsgFragment());
fragments.add(new MineFragment());
pageTitle.add("首页");
pageTitle.add("消息");
pageTitle.add("我的");
ActivityFragmentAdapter activityFragmentAdapter = new ActivityFragmentAdapter(getSupportFragmentManager());
viewPager.setAdapter(activityFragmentAdapter);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
private void initTab() {
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
class ActivityFragmentAdapter extends FragmentPagerAdapter {
public ActivityFragmentAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
// 这里设置的文字值将会在tab上显示
// @Override
// public CharSequence getPageTitle(int position) {
// return pageTitle.get(position);
// }
}
}
这里需要注意的一点事tab文字和图片的设置一定要放在viewpager和fragment关联之后或者在getPageTitle方法中仅设置显示文字,否则设置的文字不会显示。
2. BaseFragment
接下来就要设计Fragment,寿险我们要设计一个基类,用作实现懒加载。所谓懒加载就是只有当fragment显示且无数据才加载数据,当数据加载完毕,我们要将其显示到控件,所以此时控件必须已加载完毕,否则会报空指针,综上,我们共需要三个判断。
public abstract class BaseFragment extends Fragment {
private boolean isInitData = false;//是否初始化数据
private boolean isVisibleToUser = false;//是否可见
private boolean isPrepareView = false;//view是否加载完成
protected List newsDatas;//子fragment对应的tab集合
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(getLayoutId(), container, false);
}
protected abstract int getLayoutId();
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
newsDatas = new SharedPreferencesHelper(BaseApplication.getContext(), "newsDatasSave").getSharedPreferencesList("newsDatas");
isPrepareView = true;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
lazyInitData();
}
private void lazyInitData() {
if(!isInitData&&isVisibleToUser&&isPrepareView){
isInitData = true;
initData();
}
}
protected abstract void initData();
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
lazyInitData();
}
}
onViewCreated方法执行我们可以获取到view已初始化完毕的信息,setUserVisibleHint方法获取的是此fragment是否呈现的信息,当呈现时我们可以试着做第一次懒加载,如果无数据获取则讲数据获取的值改为true,并进行数据获取。为了防止fragment第一次可见未加载数据,在onActivityCreated再次做一个加载判断。
由于我最初的设想是显示新闻内容的fragment个数是可变的,而且fragment只有一个类,所以要获取页卡是什么新闻类型,最简单的实现应该就是把这两个参数放到基类里面,父tab显示的新闻类型在基类中获取。
3. MainFragment
显示新闻页卡的父fragment,布局实现和MainActivity差不多
public class MainFragment extends BaseFragment {
private TabLayout tabLayout;
private ViewPager viewPager;
@Override
protected int getLayoutId() {
return R.layout.fragment_main;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
tabLayout = (TabLayout) view.findViewById(R.id.fragment_main_tab);
viewPager = (ViewPager) view.findViewById(R.id.fragment_main_pager);
initTab();
initPager();
tabLayout.setupWithViewPager(viewPager);
for (int i = 0; i < newsDatas.size(); i++) {
tabLayout.getTabAt(i).setText(newsDatas.get(i));
}
}
private void initTab() {
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
private void initPager() {
final List fragments = new ArrayList<>();
for (int i = 0; i < newsDatas.size(); i++) {
NewsFragment newsFragment = new NewsFragment();
fragments.add(newsFragment);
newsFragment.setPosition(i);
}
viewPager.setAdapter(new FragmentPagerAdapter(getChildFragmentManager()) {
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
// @Override
// public CharSequence getPageTitle(int position) {
// return newsDatas.get(position);
// }
});
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
@Override
protected void initData() {
}
}
这里要注意的问题是新闻fragment集合fragments的初始化,fragment经常会出现突然多一块白色区域,嵌套时多了白色页卡或者页卡被挤在一旁等等,这些要注意的问题是fragment中view大小设置和viewpager的list个数变化,此demo中如果fragments在类中初始化话,方法initPager中就会不断添加list数据,导致视图被挤在一旁。
还有一点需要注意就是FragmentManager 的使用,总的来说三种获取方式:getFragmentManager()、getSupportFragmentManager()和getChildFragmentManager(),Android V4扩展包获取的FragmentManager 使用getSupportFragmentManager(),android.app获取使用getFragmentManager(),像这种嵌套的使用getChildFragmentManager()。我就直接说使用,具体区别有兴趣的可以去搜下。
4. NewsFragment
显示新闻的fragment,简单期间,我就直接用RecyclerView显示标题实现了
public class NewsFragment extends BaseFragment {
private List newsDatasValue = new ArrayList<>();//子fragment内获取的新闻内容
private RecyclerView recyclerView;
private NewsAdapter newsAdapter = new NewsAdapter();
private int position;
@Override
protected int getLayoutId() {
return R.layout.fragment_main_news;
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = (RecyclerView) view.findViewById(R.id.fragment_main_news_recycler);
recyclerView.setLayoutManager(new LinearLayoutManager(BaseApplication.getContext()));
recyclerView.setAdapter(newsAdapter);
}
@Override
protected void initData() {
String url = "";
switch (newsDatas.get(position)) {
case "头条":
url = "http://v.juhe.cn/toutiao/index?type=top&key=28aa4ac2a689c881a4e6e51cf918d695";
break;
case "社会":
url = "http://v.juhe.cn/toutiao/index?type=shehui&key=28aa4ac2a689c881a4e6e51cf918d695";
break;
case "科技":
url = "http://v.juhe.cn/toutiao/index?type=keji&key=28aa4ac2a689c881a4e6e51cf918d695";
break;
default:
url = "http://v.juhe.cn/toutiao/index?type=top&key=28aa4ac2a689c881a4e6e51cf918d695";
break;
}
OkHttpUtils
.get()
.url(url)
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
}
@Override
public void onResponse(String response, int id) {
NewsBean newsBean = new Gson().fromJson(response, NewsBean.class);
if (newsBean.getError_code() == 0) {
newsDatasValue = newsBean.getResult().getData();
newsAdapter.notifyDataSetChanged();
}
}
});
}
class NewsAdapter extends RecyclerView.Adapter {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(BaseApplication.getContext()).inflate(R.layout.item_news, parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.tv.setText(newsDatasValue.get(position).getTitle());
}
@Override
public int getItemCount() {
return newsDatasValue.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tv;
public ViewHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.item_news_tv);
}
}
}
//fragment所在页卡序号
public void setPosition(int position){
this.position = position;
}
}
还是很简单的,主要就是要注意定义fragment页卡所在序号用来区分fragment。
三、总结
其实像ViewPager+Fragment+ViewPager+Fragment这种不像知识点,更像是思路,当有这个需求的时候,又没做过,可以参考一下这个思路,然后根据自身情况再修改,有想法的人应该会看着博客写,出来的东西却完全不一样,如果真的用到项目上,肯定要提取Adapter,甚至写出各种不同的Adapter基类,还要抽离网络框架,界面设计的更美,要考虑适配问题,可能会做很多性能优化,复用、压缩、缓存。。。。。。线程要注意线程池和信息传递这些,页面多了也会有很多问题,譬如上边说的fragment多出白色区域,还有newsPosition这样的传值问题,不过代码的乐趣就是这些吧,头脑风暴,陷入进去真的会上瘾,根本停不下来。
这是我CSDN有排名后的第一篇博客(150万+......),现在我确实是个菜鸡,但希望以后能快速的成长,也能写出更好的博客,同时也祝福看到这篇博客的码友们,工作顺利,跳槽的都能找到更好的平台,祝Android行业越来越好。不要如我现在,java后端、web前端都是核心技术人员,唯独移动开发像个打杂的,现在妹子都不联系了就想找个好平台【害羞......】