实例:根据MVP设计模式并结合okhttp,recycleview进行网络数据的下载
第一步:首先是Model中的类
Model
实体模型 bean 当前的操作设计的实体类对象
业务逻辑 IxxxBiz 定义业务逻辑的具体方法 访问底层网络数据时的具体的函数
xxxBiz 业务逻辑接口的具体实现 具体的业务逻辑方法(访问网络数据 访问数据库等)
1.Bean包下的News实体类
package com.yztc.bean;
/**
* Created by sqq on 16/9/29.
*/
public class News {
/**
* id : 495126
* Content : 0
* title : 在里约 英语竟没它好使:国人惊呆
* postdate : 2016/8/11 7:03:08
* editor : 万南
* icon : http://img1.mydrivers.com/img/20160811/759d3d8e01ff493f9f3d77f4112100c2.jpg
* desc : 出门在外,懂门外语是多么重要,这是每一个到国际赛场采访的记者的一大感受。那么在里约呢?在奥运会场馆区,你只要懂英语就可以
* reviewcount : 86
* stress :
* isdel : False
* ispass : True
*/
private int id;
private String Content;
private String title;
private String postdate;
private String editor;
private String icon;
private String desc;
private int reviewcount;
private String stress;
private String isdel;
private String ispass;
public News(String title,String icon,String desc,String editor){
this.title=title;
this.icon=icon;
this.desc=desc;
this.editor=editor;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getContent() {
return Content;
}
public void setContent(String Content) {
this.Content = Content;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPostdate() {
return postdate;
}
public void setPostdate(String postdate) {
this.postdate = postdate;
}
public String getEditor() {
return editor;
}
public void setEditor(String editor) {
this.editor = editor;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public int getReviewcount() {
return reviewcount;
}
public void setReviewcount(int reviewcount) {
this.reviewcount = reviewcount;
}
public String getStress() {
return stress;
}
public void setStress(String stress) {
this.stress = stress;
}
public String getIsdel() {
return isdel;
}
public void setIsdel(String isdel) {
this.isdel = isdel;
}
public String getIspass() {
return ispass;
}
public void setIspass(String ispass) {
this.ispass = ispass;
}
}
2.model包下的各个接口
(1)实现业务逻辑的接口
package com.yztc.model; import android.app.Activity; public interface INewsBiz { public void loadNewsData(Activity activity, OnLoadNewsListener onLoadNewsListener); }
(2)业务类的具体实现
package com.yztc.model; import android.app.Activity; import android.os.Handler; import com.squareup.okhttp.Callback; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.yztc.bean.News; import com.yztc.utils.OkHttpClientUtils; import com.yztc.utils.PaserJson; import com.yztc.utils.UrlConstant; import java.io.IOException; import java.util.List; /** * Created by sqq on 16/9/29. */ public class NewsBiz implements INewsBiz{ private Handler handler=new Handler(); @Override public void loadNewsData(Activity activity, final OnLoadNewsListener onLoadNewsListener) { String url= UrlConstant.URL;//网络访问的地址 OkHttpClientUtils.getDataAsync(activity, url, new Callback() { @Override public void onFailure(Request request, final IOException e) { handler.post(new Runnable() { @Override public void run() { onLoadNewsListener.onFailure(e.getMessage(),e); } }); } @Override public void onResponse(Response response) throws IOException { //访问网络成功获取字符串解析存储到list集合中 final List<News> list= PaserJson.parserJsonToList(response.body().string()); handler.post(new Runnable() { @Override public void run() { onLoadNewsListener.onSuccess(list); } }); } }, ""); } }
(3)下载网络数据的结果,成功或是失败
package com.yztc.model; import com.yztc.bean.News; import java.util.List; /** * Created by sqq on 16/9/29. */ public interface OnLoadNewsListener { public void onSuccess(List<News> list); public void onFailure(String msg,Exception e); }
第二步:View view层接口类 IxxxView 定义界面中对view操作的函数 比如:获取view中的数据 清除view数据 设置view数据
定义view视图层的接口 接口中定义什么函数? * 观察界面需要展示什么信息在界面上 需要展示什么 该接口中就定义什么函数向 * activity展示
package com.yztc.view; import com.yztc.adapter.RecyclerAdapter; public interface INewsView { public void toSetAdapter(RecyclerAdapter adapter); public void showErrorToast(String msg); }
第三步:
Presenter 中间人类 * 在model与view之间数据连接 * 访问model中的业务逻辑方法 展示到view层的视图中
package com.yztc.presenter; import android.app.Activity; import android.content.Context; import com.yztc.adapter.RecyclerAdapter; import com.yztc.bean.News; import com.yztc.model.INewsBiz; import com.yztc.model.NewsBiz; import com.yztc.model.OnLoadNewsListener; import com.yztc.view.INewsView; import java.util.List; public class NewsPresenter { private INewsBiz newsBiz; private INewsView newsView; public NewsPresenter(INewsView iNewsView){ this.newsView=iNewsView; this.newsBiz=new NewsBiz(); } public void LoadNewDataToUi(final Activity activity){ newsBiz.loadNewsData(activity, new OnLoadNewsListener() { @Override public void onSuccess(List<News> list) { if(list!=null && list.size()!=0){ RecyclerAdapter adapter=new RecyclerAdapter(activity,list); newsView.toSetAdapter(adapter); } } @Override public void onFailure(String msg, Exception e) { newsView.showErrorToast(e.getMessage()); } }); } }
第四步:MainActivity中
package com.yztc.mvpnewsdemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.widget.Toast; import com.yztc.adapter.RecyclerAdapter; import com.yztc.presenter.NewsPresenter; import com.yztc.utils.DividerItemDecoration; import com.yztc.view.INewsView; public class MainActivity extends AppCompatActivity implements INewsView{ private RecyclerView mRecyclerView; private NewsPresenter presenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); presenter=new NewsPresenter(this);//INewsView接口的子类 presenter.LoadNewDataToUi(this);//activity类的对象 } //初始化视图 public void initView(){ mRecyclerView= (RecyclerView) findViewById(R.id.rlv); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.HORIZONTAL_LIST)); } @Override public void toSetAdapter(RecyclerAdapter adapter) { if(adapter!=null){ mRecyclerView.setAdapter(adapter); } } @Override public void showErrorToast(String msg) { Toast.makeText(MainActivity.this,msg,Toast.LENGTH_SHORT).show(); } }至此,已完成MVP设计模式的所有工作
以下代码为各种工具类的具体代码
1.RecyclerAdapter适配器
package com.yztc.adapter; import android.content.Context; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.yztc.bean.News; import com.yztc.mvpnewsdemo.R; import java.util.List; /** * Created by sqq on 16/9/23. */ public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.MyViewHolder>{ private Context context; private List<News> list; public RecyclerAdapter(Context context,List<News> list){ this.context=context; this.list=list; } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new MyViewHolder(LayoutInflater.from(context). inflate(R.layout.list_item,null)); } @Override public void onBindViewHolder(MyViewHolder holder, int position) { //采用glide加载网络图片 Glide.with(context).load(list.get(position).getIcon()). placeholder(R.mipmap.ic_launcher).into(holder.iv); holder.tv_title.setText(list.get(position).getTitle()); holder.tv_desc.setText(list.get(position).getDesc()); holder.tv_editor.setText(list.get(position).getEditor()); } @Override public int getItemCount() { return list.size(); } class MyViewHolder extends RecyclerView.ViewHolder{ ImageView iv; TextView tv_title,tv_desc,tv_editor; public MyViewHolder(View itemView) { super(itemView); iv= (ImageView) itemView.findViewById(R.id.iv); tv_title= (TextView) itemView.findViewById(R.id.tv_subject); tv_desc= (TextView) itemView.findViewById(R.id.tv_summary); tv_editor= (TextView) itemView.findViewById(R.id.tv_changed); } } //刷新数据 public void reloadRecyclerView(List<News> _list, boolean isClear) { if (isClear) { list.clear(); } list.addAll(_list); notifyDataSetChanged(); } }
2.设置线性排列时item之间的分割线
package com.yztc.utils; /* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * limitations under the License. */ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.View;
public class DividerItemDecoration extends RecyclerView.ItemDecoration { private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; private Drawable mDivider; private int mOrientation; public DividerItemDecoration(Context context, int orientation) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); setOrientation(orientation); } public void setOrientation(int orientation) { if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { throw new IllegalArgumentException("invalid orientation"); } mOrientation = orientation; } @Override public void onDraw(Canvas c, RecyclerView parent) { if (mOrientation == VERTICAL_LIST) { drawVertical(c, parent); } else { drawHorizontal(c, parent); } } public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); RecyclerView v = new RecyclerView(parent.getContext()); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin; final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } public void drawHorizontal(Canvas c, RecyclerView parent) { final int top = parent.getPaddingTop(); final int bottom = parent.getHeight() - parent.getPaddingBottom(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int left = child.getRight() + params.rightMargin; final int right = left + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } @Override public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); } } }
3.okhttp访问网络工具类
package com.yztc.utils; import android.content.Context; import com.squareup.okhttp.Cache; import com.squareup.okhttp.Callback; import com.squareup.okhttp.FormEncodingBuilder; import com.squareup.okhttp.Headers; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.MultipartBuilder; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.FileNameMap; import java.net.URLConnection; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; public class OkHttpClientUtils { private static OkHttpClient okHttpClient = null; private static OkHttpClientUtils okHttpUtils = null; private OkHttpClientUtils(Context context) { okHttpClient = getOkHttpSingletonInstance(); //开启响应缓存 okHttpClient.setCookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)); //设置缓存目录和大小 int cacheSize = 10 << 20; // 10 MiB Cache cache = new Cache(context.getCacheDir(), cacheSize); okHttpClient.setCache(cache); //设置合理的超时 okHttpClient.setConnectTimeout(15, TimeUnit.SECONDS); okHttpClient.setReadTimeout(20, TimeUnit.SECONDS); okHttpClient.setWriteTimeout(20, TimeUnit.SECONDS); //以下验证不设置,那么默认就已经设置了验证 okHttpClient.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); } public static OkHttpClientUtils getOkHttpClientUtils(Context context) { if (okHttpUtils == null) { synchronized (OkHttpClientUtils.class) { if (okHttpUtils == null) { okHttpUtils = new OkHttpClientUtils(context); } } } return okHttpUtils; } public static OkHttpClient getOkHttpSingletonInstance() { if (okHttpClient == null) { synchronized (OkHttpClient.class) { okHttpClient = new OkHttpClient(); } } return okHttpClient; } /// // GET方式网络访问 /// /** * 基方法,返回Request对象 * * @param urlString * @param tag * @return */ private Request buildGetRequest(String urlString, Object tag) { Request.Builder builder = new Request.Builder(); builder.url(urlString); if (tag != null) { builder.tag(tag); } return builder.build(); } /** * 自定义方法,返回Response对象 * * @param urlString * @return * @throws IOException */ private Response buildResponse(String urlString, Object tag) throws IOException { Request request = buildGetRequest(urlString, tag); Response response = okHttpClient.newCall(request).execute(); return response; } //基础方法,返回ResponseBody对象 private ResponseBody buildResponseBody(String urlString, Object tag) throws IOException { Response response = buildResponse(urlString, tag); if (response.isSuccessful()) { return response.body(); } return null; } /** * 作用:实现网络访问文件,将获取到数据储存在文件流中 * * @param urlString :访问网络的url地址 * @return InputStream */ public static InputStream getStreamFromURL(Context context, String urlString, Object tag) throws IOException { ResponseBody body = getOkHttpClientUtils(context).buildResponseBody(urlString, tag); if (body != null) { return body.byteStream(); } return null; } /** * 作用:实现网络访问文件,将获取到的数据存在字节数组中 * * @param urlString :访问网络的url地址 * @return byte[] */ public static byte[] getBytesFromURL(Context context, String urlString, Object tag) throws IOException { ResponseBody body = getOkHttpClientUtils(context).buildResponseBody(urlString, tag); if (body != null) { return body.bytes(); } return null; } /** * 作用:实现网络访问文件,将获取到的数据存在字符串中 * * @param urlString :访问网络的url地址 * @return String */ public static String getStringFromURL(Context context, String urlString, Object tag) throws IOException { ResponseBody body = getOkHttpClientUtils(context).buildResponseBody(urlString, tag); if (body != null) { return body.string(); } return null; } /// // POST方式访问网络 /// /** * 基方法,返回Request对象 * * @param urlString * @param tag * @return */ private Request buildPostRequest(String urlString, RequestBody requestBody, Object tag) { Request.Builder builder = new Request.Builder(); builder.url(urlString).post(requestBody); //builder.addHeader("Accept", "application/json; q=0.5"); if (tag != null) { builder.tag(tag); } return builder.build(); } /** * 作用:post提交数据,返回服务器端返回的字节数组 * * @param urlString :访问网络的url地址 * @return byte[] */ private String postRequestBody(String urlString, RequestBody requestBody, Object tag) { Request request = buildPostRequest(urlString, requestBody, tag); try { Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { return response.body().string(); } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 作用:POST提交键值对,再返回相应的数据 * * @param urlString :访问网络的url地址 * @param map :访问url时,需要传递给服务器的键值对数据。 * @return String */ public static String postKeyValuePair(Context context, String urlString, Map<String, String> map, Object tag) { //往FormEncodingBuilder对象中放置键值对 FormEncodingBuilder formBuilder = new FormEncodingBuilder(); if (map != null && !map.isEmpty()) { for (Map.Entry<String, String> entry : map.entrySet()) { formBuilder.add(entry.getKey(), entry.getValue()); } } //生成请求体对象 RequestBody requestBody = formBuilder.build(); //将请求提放置到请求对象中 return getOkHttpClientUtils(context).postRequestBody(urlString, requestBody, tag); } /** * 作用:POST提交Json字符串,再返回相应的数据 * * @param urlString :访问网络的url地址 * @param jsonString :访问url时,需要传递给服务器的json字符串 * @return byte[] */ public static String postJsonString(Context context, String urlString, String jsonString, Object tag) { //定义mimetype对象 /*String MEDIA_TYPE_STREAM = "application/octet-stream;charset=utf-8"; String MEDIA_TYPE_STRING = "text/plain;charset=utf-8";*/ String MEDIA_TYPE_JSON = "application/json;charset=utf-8"; MediaType JSON = MediaType.parse(MEDIA_TYPE_JSON); RequestBody requestBody = RequestBody.create(JSON, jsonString); return getOkHttpClientUtils(context).postRequestBody(urlString, requestBody, tag); } /// // 异步网络访问 /// /** * 开启异步线程访问网络,通过回调方法实现数据加载 * 如果第二个参数为null,空callback, 则说明不在意返回结果 * * @param urlString * @param callback */ public static void getDataAsync(Context context, String urlString, Callback callback, Object tag) { Request request = getOkHttpClientUtils(context).buildGetRequest(urlString, tag); getOkHttpSingletonInstance().newCall(request).enqueue(callback); } /** * 作用:post提交数据,返回服务器端返回的字节数组 * * @param urlString :访问网络的url地址 */ private void postRequestBodyAsync(String urlString, RequestBody requestBody, Callback callback, Object tag) { Request request = buildPostRequest(urlString, requestBody, tag); if (callback == null) { new Callback() { @Override public void onFailure(Request request, IOException e) { } @Override public void onResponse(Response response) throws IOException { } }; } okHttpClient.newCall(request).enqueue(callback); } /** * 作用:POST提交键值对,再返回相应的数据 * * @param urlString :访问网络的url地址 * @param map :访问url时,需要传递给服务器的键值对数据。 */ public static void postKeyValuePairAsync(Context context, String urlString, Map<String, String> map, Callback callback, Object tag) { //往FormEncodingBuilder对象中放置键值对 FormEncodingBuilder formBuilder = new FormEncodingBuilder(); if (map != null && !map.isEmpty()) { for (Map.Entry<String, String> entry : map.entrySet()) { formBuilder.add(entry.getKey(), entry.getValue()); } } //生成请求体对象 RequestBody requestBody = formBuilder.build(); //将请求提放置到请求对象中 getOkHttpClientUtils(context).postRequestBodyAsync(urlString, requestBody, callback, tag); } /** * 作用:post异步上传文件,提交分块请求 * * @param urlString 网络地址 * @param map 提交给服务器的表单信息键值对 * @param files 提交的文件 * @param formFieldName 每个需要提交的文件对应的文件input的name值 * @param callback 异步上传回调方法 * @throws IOException */ public static void postUploadFilesAsync(Context context, String urlString, Map<String, String> map, File[] files, String[] formFieldName, Callback callback, Object tag) throws IOException { RequestBody requestBody = getOkHttpClientUtils(context).buildRequestBody(map, files, formFieldName); getOkHttpClientUtils(context).postRequestBodyAsync(urlString, requestBody, callback, tag); } /// // POST方式提交分块请求,实现文件上传 /// /** * 同步基于post的文件上传:上传多个文件以及携带key-value对:主方法 * * @param urlString * @param formFiledName * @param files * @param map * @param tag * @return String * @throws IOException */ public static String postUploadFiles(Context context, String urlString, Map<String, String> map, File[] files, String[] formFiledName, Object tag) throws IOException { RequestBody requestBody = getOkHttpClientUtils(context).buildRequestBody(map, files, formFiledName); return getOkHttpClientUtils(context).postRequestBody(urlString, requestBody, tag); } /** * 创建post上传附件的request对象 * Post方式提交分块请求——上传文件及其它表单数据 * * @param files * @param formFiledName * @param map * @return */ private RequestBody buildRequestBody(Map<String, String> map, File[] files, String[] formFiledName) { MultipartBuilder builder = new MultipartBuilder().type(MultipartBuilder.FORM); //往MultipartBuilder对象中添加普通input控件的内容 if (map != null) { for (Map.Entry<String, String> entry : map.entrySet()) { //添加普通input块的数据 builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + entry .getKey() + "\""), RequestBody.create(null, entry.getValue())); } } //往MultipartBuilder对象中添加file input控件的内容 if (files != null && formFiledName != null) { for (int i = 0; i < files.length; i++) { File file = files[i]; String fileName = file.getName(); RequestBody requestBody = RequestBody.create(MediaType.parse ("multipart/form-data"), file); //添加file input块的数据 builder.addPart(Headers.of("Content-Disposition", "form-data; name=\"" + formFiledName[i] + "\"; filename=\"" + fileName + "\""), requestBody); } } //生成RequestBody对象 return builder.build(); } /** * 获取Mime类型 * * @param filename * @return */ private static String getMimeType(String filename) { FileNameMap fileNameMap = URLConnection.getFileNameMap(); String contentTypeFor = fileNameMap.getContentTypeFor(filename); if (contentTypeFor == null) { contentTypeFor = "application/octet-stream"; } return contentTypeFor; } public static void cancelCall(Object tag) { getOkHttpSingletonInstance().cancel(tag); } }
4.原生的json解析工具类
package com.yztc.utils; import com.yztc.bean.News; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; /** * Created by sqq on 16/9/24. */ public class PaserJson { public static List<News> parserJsonToList(String jsonString){ List<News> list=new ArrayList<>(); try { JSONArray array=new JSONArray(jsonString); for(int i=0;i<array.length();i++){ JSONObject object=array.getJSONObject(i); String title=object.getString("title"); String desc=object.getString("desc"); String editor=object.getString("editor"); String icon=object.getString("icon"); News news=new News(title,icon,desc,editor); list.add(news); } } catch (Exception e) { e.printStackTrace(); } return list; } }
5.网络数据地址
package com.yztc.utils; /** * Created by sqq on 16/9/28. */ public class UrlConstant { public static final String URL="http://m.mydrivers.com/app/newslist.aspx?tid=0&minId=495127&maxId=0&ver=2.2&temp=1470901806343"; }
activity_main.xml文件中
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/rlv" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
适配器中的布局文件
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv" android:layout_width="80dp" android:layout_height="80dp" android:src="@mipmap/ic_launcher"/> <TextView android:id="@+id/tv_subject" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/iv" android:text="Subject" android:textColor="#aa0000" android:textSize="20sp"/> <TextView android:id="@+id/tv_summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_subject" android:layout_toRightOf="@id/iv" android:text="Summary" android:singleLine="true" android:textSize="18sp"/> <TextView android:id="@+id/tv_changed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/tv_summary" android:layout_toRightOf="@id/iv" android:text="changed" android:textSize="18sp"/> </RelativeLayout>