您的位置:首页 > 移动开发

安卓开发笔记——打造属于自己的博客园APP(四)

2015-08-27 19:58 656 查看
  在上篇文章《安卓开发笔记——打造属于自己的博客园APP(三)》中,我们对博客文章的详情页和评论页进行了实现,慢慢的一个APP已经出现雏形了,当然这只是完成了"表面效果",要真正做好一个APP并不是一件很轻松的事情,有很多细节需要我们一点一滴的去完善。

  好了,来讲下今天要完成的效果,在优化了之前部分代码的前提下,今天来说下关于博客搜索和博客详情页的实现,依旧国际惯例,来看下效果图:(动态图片比较大,加载需要点时间)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:orientation="horizontal"
android:padding="3dp">
<!--头像-->
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_userhead"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:src="@mipmap/avatar_default"
app:riv_border_color="#ffffff"
app:riv_border_width="2dip"
app:riv_corner_radius="30dip"
app:riv_mutate_background="true"
app:riv_oval="true"
app:riv_tile_mode="repeat" />
<!--信息内容-->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_weight="1"
android:orientation="vertical">

<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:text="测试标题"
android:textColor="@color/md_grey_900"
android:textSize="16sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:maxLines="3"
android:text="浏览器类型判断方法有两种:根据浏览器特性来判断根据来检测具体使用哪种方法要看具体需求的场景场景一:为了让用户有较流畅完整的体验,在站点提示用户使用或者,这种场景对浏览器类型的判断并非特别严格,可以使用检测的方法。(因为很多浏览器厂商会篡改标识)。场景二...."
android:textSize="14sp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="1dp"
android:layout_weight="1"
android:orientation="horizontal">

<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="博文数:"
android:textColor="@color/md_grey_500"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_sum"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginRight="5dp"
android:gravity="center_vertical"
android:textColor="@color/md_grey_500"
android:textSize="12sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="最后更新:"
android:textColor="@color/md_grey_500"
android:textSize="12sp" />

<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginRight="5dp"
android:textColor="@color/md_grey_500"
android:textSize="12sp" />

</LinearLayout>

</LinearLayout>

<!--操作按钮-->
<ImageButton
android:id="@+id/ib_more"
android:layout_width="25dp"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground"
android:gravity="center_horizontal"
android:src="@mipmap/triangle" />
</LinearLayout>


recyclerview_item_authorlist_content.xml
关于博客园推荐博客的接口数据XML解析,和我们之前在做博文列表数据的解析都是一样的,只不过是XML的节点不同,这里贴出解析代码:

package com.lcw.rabbit.myblog.parser;

import com.lcw.rabbit.myblog.entity.Author;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
* 对博客作者(列表)xml数据的解析
* Created by Lichenwei
* Date: 2015-08-17
* Time: 13:32
*/
public class AuthorListXmlParser {

/**
* 用于解析博客作者(列表)的xml,返回Avatar的List集合对象
*
* @param inputStream
* @param encode
* @return
* @throws XmlPullParserException
* @throws IOException
*/
public static List<Author> getListAuthor(InputStream inputStream, String encode) throws XmlPullParserException, IOException {

List<Author> mAuthors = null;
Author mAuthor = null;

//获取XmlPullParser实例
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(inputStream, encode);
//获取解析事件
int eventType = parser.getEventType();
//当xml文档未到尾端时
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
//解析根标签的时候,实例化集合
case XmlPullParser.START_DOCUMENT:
mAuthors = new ArrayList<Author>();
mAuthor = new Author();

break;
case XmlPullParser.START_TAG:
//当解析到entry标签的时候,实例化Avatar对象
if ("entry".equals(parser.getName())) {
mAuthor = new Author();
}
if ("id".equals(parser.getName())) {
parser.next();
mAuthor.setAuthorUrl(parser.getText());
} else if ("title".equals(parser.getName())) {
parser.next();
if (parser.getText().indexOf("博客园") == -1) {
mAuthor.setAuthorName(parser.getText());
}
} else if ("avatar".equals(parser.getName())) {
parser.next();
mAuthor.setAuthorPic(parser.getText());
} else if ("blogapp".equals(parser.getName())) {
parser.next();
mAuthor.setBlogApp(parser.getText());
} else if ("postcount".equals(parser.getName())) {
parser.next();
mAuthor.setBlogCount(parser.getText());
} else if ("updated".equals(parser.getName())) {
parser.next();
//区分日期格式
if (parser.getText().indexOf("+") != -1) {
mAuthor.setUpdated(parser.getText());
}

}
break;
case XmlPullParser.END_TAG:
//当解析到entry标签结束的时候添加入Avatar集合,清空Avatar对象
if ("entry".equals(parser.getName())) {
mAuthors.add(mAuthor);
mAuthor = null;
}
break;

}
//手动跳转第一次遍历
eventType = parser.next();
}

return mAuthors;

}

}


  这里是搜索主页面代码,当我们点击搜索按钮的时候,获取输入框的博客关键字,进行搜索,将得到的数据更新数据源重新展示在RecyclerView。

  关于搜索博客关键字的接口:http://wcf.open.cnblogs.com/blog/bloggers/search?t={TERM} {TERM}代表作者名(多关键字匹配),搜索到的XML数据信息刚好又和我们前面的推荐博客内容格式一致,所以我们可以复用之前的解析工具类,然后一样具备上拉刷新和下拉加载功能:

package com.lcw.rabbit.myblog;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Toast;

import com.afollestad.materialdialogs.MaterialDialog;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.lcw.rabbit.myblog.adapter.AuthorListAdapter;
import com.lcw.rabbit.myblog.entity.Author;
import com.lcw.rabbit.myblog.entity.Blog;
import com.lcw.rabbit.myblog.parser.AuthorListXmlParser;
import com.lcw.rabbit.myblog.utils.VolleyRequestQueueManager;
import com.lcw.rabbit.myblog.view.MyProgressBar;
import com.mugen.Mugen;
import com.mugen.MugenCallbacks;
import com.mugen.attachers.BaseAttacher;

import org.xmlpull.v1.XmlPullParserException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* 博客搜索页
*/
public class SearchActivity extends AppCompatActivity {

//界面控件
private Toolbar mToolbar;
private EditText mEditText;
private ImageButton mImageButton;
private RecyclerView mRecyclerView;
private MaterialDialog materialDialog;
//无限滚动
private BaseAttacher mBaseAttacher;
private MyProgressBar myProgressBar;

//数据源
private AuthorListAdapter mAuthorListAdapter;
private List<Author> mAuthors;

//标志变量
private boolean isLoading = false;
private int currentPage = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
initView();
initData();
initAction();
}

/**
* 设置控件监听
*/
private void initAction() {
//设置无限滚动,上拉加载
mBaseAttacher = Mugen.with(mRecyclerView, new MugenCallbacks() {
@Override
public void onLoadMore() {
//加载更多
isLoading = true;
mBaseAttacher.setLoadMoreEnabled(false);
myProgressBar.setVisibility(View.VISIBLE);
getData((currentPage + 1), 10);
}

@Override
public boolean isLoading() {
return isLoading;
}

@Override
public boolean hasLoadedAllItems() {
return isLoading;
}
}).start();
//离底部一项的时候加载更多
mBaseAttacher.setLoadMoreOffset(1);

//点击搜索
mImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
materialDialog = new MaterialDialog.Builder(SearchActivity.this).content("内容加载中..").progress(true, 0).show();
String name = mEditText.getText().toString();
getDataByName(name.trim());
}
});

//设置RecyclerView点击监听
mAuthorListAdapter.setRecyclerViewListener(new AuthorListAdapter.RecyclerViewListener() {
@Override
public void setOnclickListener(View view, String value) {
//封装Bolg对象传递
Blog blog = new Blog();
blog.setBlogApp(mAuthors.get(Integer.parseInt(value)).getBlogApp());
blog.setAuthorName(mAuthors.get(Integer.parseInt(value)).getAuthorName());
Intent intent = new Intent();
intent.setClass(SearchActivity.this, AuthorActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("blog", blog);
intent.putExtras(bundle);
startActivity(intent);
}
});
}

/**
* 初始化控件
*/
private void initView() {
//ToolBar
mToolbar = (Toolbar) findViewById(R.id.activity_toolbar);
setSupportActionBar(mToolbar);
//需要放在setSupportActionBar后设置
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
getSupportActionBar().setTitle("博客搜索");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

//输入搜索
mEditText = (EditText) findViewById(R.id.et_text);
mImageButton = (ImageButton) findViewById(R.id.ib_search);
//列表
mRecyclerView = (RecyclerView) findViewById(R.id.rv_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
//当高度确定,提高效率
mRecyclerView.setHasFixedSize(true);
//友好提醒
myProgressBar = (MyProgressBar) findViewById(R.id.progressbar);

}

/**
* 初始化数据
*/
private void initData() {
//显示下拉刷新样式
mAuthors = new ArrayList<Author>();
//设置空数据
mAuthorListAdapter = new AuthorListAdapter(this, mAuthors);
mRecyclerView.setAdapter(mAuthorListAdapter);
getData(1, 10);
}

/**
* 根据博主昵称搜索博客
*
* @param name
*/
public void getDataByName(String name) {
String url = "http://wcf.open.cnblogs.com/blog/bloggers/search?t=" + name;
StringRequest request = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
@Override
public void onResponse(String s) {
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes());
List<Author> authors = AuthorListXmlParser.getListAuthor(inputStream, "utf-8");
if (authors.size() != 0) {
//清空之前的数据预防重复加载
mAuthors.clear();
for (Author author : authors) {
mAuthors.add(author);
}

if (mAuthorListAdapter == null) {
mAuthorListAdapter = new AuthorListAdapter(SearchActivity.this, mAuthors);
mRecyclerView.setAdapter(mAuthorListAdapter);
} else {
//通知adatper数据源更新
mAuthorListAdapter.refreshData(mAuthors);
}

materialDialog.dismiss();
} else {
//如果匹配不到关键字
Toast.makeText(SearchActivity.this, "找不到相关用户,请重新搜索", Toast.LENGTH_SHORT).show();
materialDialog.dismiss();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
Toast.makeText(SearchActivity.this, volleyError.getMessage(), Toast.LENGTH_SHORT).show();
}
});

VolleyRequestQueueManager.addRequest(request, "getAuthorList");

}

/**
* 获取推荐数据
*
* @param page
* @param num
*/
public void getData(final int page, int num) {
this.currentPage = page;
String url = "http://wcf.open.cnblogs.com/blog/bloggers/recommend/" + page + "/" + num;
StringRequest request = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
@Override
public void onResponse(String s) {
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes());
List<Author> authors = AuthorListXmlParser.getListAuthor(inputStream, "utf-8");
if (page == 1) {
//清空之前的数据预防重复加载
mAuthors.clear();
}
for (Author author : authors) {
mAuthors.add(author);
}

if (mAuthorListAdapter == null) {
mAuthorListAdapter = new AuthorListAdapter(SearchActivity.this, mAuthors);
mRecyclerView.setAdapter(mAuthorListAdapter);
} else {
//通知adatper数据源更新
mAuthorListAdapter.refreshData(mAuthors);
}

isLoading = false;
mBaseAttacher.setLoadMoreEnabled(true);
myProgressBar.setVisibility(View.GONE);

} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError volleyError) {
Toast.makeText(SearchActivity.this, volleyError.getMessage(), Toast.LENGTH_SHORT).show();
}
});

VolleyRequestQueueManager.addRequest(request, "getAuthorList");

}

}


这里是博客列表的适配器类,和之前的博文列表一样,我们需要自己添加点击监听,再点击Item项的时候将Blog对象传递:

package com.lcw.rabbit.myblog.adapter;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;

import com.lcw.rabbit.myblog.R;
import com.lcw.rabbit.myblog.entity.Author;
import com.lcw.rabbit.myblog.utils.ImageCacheManager;
import com.lcw.rabbit.myblog.utils.TimeUtil;
import com.makeramen.roundedimageview.RoundedImageView;

import java.util.List;

/**
* 推荐博主列表适配器
* Created by Lichenwei
* Date: 2015-08-16
* Time: 22:34
*/
public class AuthorListAdapter extends RecyclerView.Adapter<AuthorListAdapter.RecyclerViewViewHolder> {

private Context mContext;
private List<Author> mAuthors;

public AuthorListAdapter(Context context, List<Author> authors) {
this.mContext = context;
this.mAuthors = authors;
}

/**
* 设置新的数据源,提醒adatper更新
*
* @param authors
*/
public void refreshData(List<Author> authors) {
this.mAuthors = authors;
this.notifyDataSetChanged();
}

/**
* 自定义点击回调接口
*/
public interface RecyclerViewListener {
void setOnclickListener(View view, String value);
}

private RecyclerViewListener mRecyclerViewListener;

/**
* 提供setter方法
*
* @param recyclerViewListener
*/
public void setRecyclerViewListener(RecyclerViewListener recyclerViewListener) {
this.mRecyclerViewListener = recyclerViewListener;
}

/**
* 创建ViewHolder
*
* @param viewGroup
* @param i
* @return
*/
@Override
public RecyclerViewViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recyclerview_item_authorlist, viewGroup, false);
return new RecyclerViewViewHolder(view);
}

/**
* 根据资源ID返回Bitmap对象
*
* @param resId
* @return
*/
public Bitmap getBitmapFromRes(int resId) {
Resources res = mContext.getResources();
return BitmapFactory.decodeResource(res, resId);

}

/**
* 绑定数据
*
* @param viewholder
* @param i
*/
@Override
public void onBindViewHolder(RecyclerViewViewHolder viewholder, int i) {
//设置头像
if (mAuthors.get(i).getAuthorPic() != null && !"".equals(mAuthors.get(i).getAuthorPic())) {
ImageCacheManager.loadImage(mAuthors.get(i).getAuthorPic(), viewholder.mUserhead, getBitmapFromRes(R.mipmap.avatar_default), getBitmapFromRes(R.mipmap.avatar_default));
} else {
viewholder.mUserhead.setImageResource(R.mipmap.avatar_default);
}
viewholder.mName.setText(mAuthors.get(i).getAuthorName());
viewholder.mUrl.setText(mAuthors.get(i).getAuthorUrl());
viewholder.mSum.setText(mAuthors.get(i).getBlogCount());
viewholder.mTime.setText(mAuthors.get(i).getUpdated());
//处理日期特殊格式
String date = TimeUtil.DateToChineseString(TimeUtil.ParseUTCDate(mAuthors.get(i).getUpdated()));
viewholder.mTime.setText(date);

//设置点击监听
viewholder.itemView.setTag(i);
viewholder.mMore.setTag(i);
viewholder.itemView.setOnClickListener(new ItemClick());
viewholder.mMore.setOnClickListener(new ItemClick());
}

@Override
public int getItemCount() {
return mAuthors.size();
}

/**
* 自定义ViewHolder
*/
public static class RecyclerViewViewHolder extends RecyclerView.ViewHolder {
private RoundedImageView mUserhead;
private TextView mName;
private TextView mUrl;
private TextView mTime;
private TextView mSum;
private ImageButton mMore;

public RecyclerViewViewHolder(View view) {
super(view);
mUserhead = (RoundedImageView) view.findViewById(R.id.iv_userhead);
mName = (TextView) view.findViewById(R.id.tv_name);
mUrl = (TextView) view.findViewById(R.id.tv_url);
mTime = (TextView) view.findViewById(R.id.tv_time);
mSum = (TextView) view.findViewById(R.id.tv_sum);
mMore = (ImageButton) view.findViewById(R.id.ib_more);

}

}

/**
* 点击事件实现类
*/
public class ItemClick implements View.OnClickListener {
@Override
public void onClick(View v) {
if (mRecyclerViewListener != null) {
mRecyclerViewListener.setOnclickListener(v, String.valueOf(v.getTag()));
}
}
}
}


2、关于博客首页的实现:

  页面的整体结构还是类似,上半部分是信息展示,下半部分是一个RecyclerView列表,列表项里面的布局和相关操作下拉刷新上拉加载,我们都可以复用之前首页的博文展示,这里只不过是数据源的不同。

  由于我们在搜索页面点击Item项的时候传递了Blog对象,我们就可以根据作者名来获取关于博客作者的一些信息,这里是接口:http://wcf.open.cnblogs.com/blog/u/{BLOGAPP}/posts/{PAGEINDEX}/{PAGESIZE} {BLOGAPP}代表作者的账号,{PAGEINDEX}代表页码,{PAGESIZE}代表每页的数据条数。

  重复的东西这里就不再多说了,这里讲点新引入的东西,细心的朋友可能有发现当我们更新数据(上拉刷新,下拉加载)的时候,列表底部有个弹出提示框,这个就是安卓5.0用来取代Toast的SnackBar:

            //snackbar提醒
Snackbar snackbar = Snackbar.make(mToolbar,"当前更新"+mBlogs.size()+"条博客信息",Snackbar.LENGTH_LONG);
snackbar.show();


用法很简单,类似于Toast,不过不一样的是,它是具有交互效果的,也就是说我们可以在Snackbar上添加点击事件,关于Snackbar的详细用法,有兴趣的朋友自己网上找找资料吧。

  然后再来说下关于这个FAB(Floating Action Button)的移动效果,这里我采用的是共享元素移动,由于我们的博文详情页和博主详情页都具有这个控件,所以我们在切换Activity的时候可以进行动画设置,这里makeSceneTransitionAnimation的第二个参数为共享View,第三个参数为标志参数要与xml里的transitionName保持一致。

android:transitionName="fab"


startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(AuthorActivity.this, mFloatingActionButton, "fab").toBundle());


好了,今天先写到这里,改天继续更新,有什么建议或疑问,可以在文章评论给我留言。

作者:李晨玮
出处:http://www.cnblogs.com/lichenwei/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
正在看本人博客的这位童鞋,我看你气度不凡,谈吐间隐隐有王者之气,日后必有一番作为!旁边有“推荐”二字,你就顺手把它点了吧,相得准,我分文不收;相不准,你也好回来找我!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: