1. 项目搭建
1.1. 项目分包
包结构划分。项目比较复杂时,大家开始动手完成代码前必须要想清楚,代码是放在哪里的。
1.2. 依赖配置
Project配置build.gradle
添加apt工具
classpath'com.neenbedankt.gradle.plugins:android-apt:1.8'
如图
模块配置使用插件
applyplugin:'com.neenbedankt.android-apt'
如图
依赖配置
// dagger2
compile 'com.google.dagger:dagger:2.6'
apt 'com.google.dagger:dagger-compiler:2.6'
// 添加ButterKnife
compile 'com.jakewharton:butterknife:5.1.1'
// 网络访问工具
compile 'com.google.code.gson:gson:2.2.4'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
// 数据库操作工具
compile 'com.j256.ormlite:ormlite-android:5.0'
如图
1.3. 通用的工具类
1.3.1. 全局上下文
com.itheima.takeout.MyApplication
public classMyApplicationextends Application {
private staticMyApplicationinstance;
public staticMyApplication getInstance() {
returninstance;
}
@Override
public voidonCreate() {
super.onCreate();
instance=this;
}
}
注意如果出现空指针可能是忘记配置Applilcation
app/src/main/AndroidManifest.xml
<application
android:name=".MyApplication"
1.3.2. 全局常量类
com.itheima.takeout.utils.Constant
public interfaceConstant {
//http://localhost:8080/TakeoutService/login?username="itheima"&password="bj"
StringHOME="http://10.0.2.2:8080/";
String LOGIN="TakeoutService/login";
}
1.3.3. 基类BaseFragment
com.itheima.takeout.ui.fragment.BaseFragment
public classBaseFragmentextends Fragment{
}
1.3.4. 基类BaseActivity
com.itheima.takeout.ui.activity.BaseActivity
public classBaseActivityextends AppCompatActivity{
//TODO 因网络状态不同显示不同界面的处理
//TODO 因定位状态不同显示不同界面的处理
// 无法获取定位:没有网络,无GPS信号
@Override
protected voidonCreate(@NullableBundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
1.4. Model核心数据
1.4.1. 数据库ormlite
com.itheima.takeout.model.dao.DBHelper
public classDBHelperextends OrmLiteSqliteOpenHelper {
private static finalString DATABASENAME = "itheima.db";
private static final intDATABASEVERSION= 1;
privateDBHelper(Context context) {
super(context,DATABASENAME,null,DATABASEVERSION);
}
private staticDBHelperinstance;
public staticDBHelper getInstance(Context context) {
if(instance== null) {
instance= newDBHelper(context);
}
returninstance;
}
@Override
public voidonCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
//TODO 表的创建
}
@Override
public voidonUpgrade(SQLiteDatabase database, ConnectionSource connectionSource,int oldVersion, int newVersion) {
//TODO 表的更新
}
}
1.4.2. 网络层retrofit
配置联网权限
<uses-permissionandroid:name="android.permission.INTERNET"/>
服务端通过设计返回的结果都是以下格式
格式为:
{ "code": "0", "data": "{……}" }
|
不同请求回复的内容区别在于data区域,
code为服务器处理的状态,“0”代表成功,非“0”为服务器处理失败,内容参考错误提示信息对照表。
而以下说明主要以data中数据为主。
com.itheima.takeout.model.net.bean.ResponseInfo
public classResponseInfo {
publicString code;
publicString data;
}
配置请求方法,将来这些方法上的参数被retrofit读取
public interfaceResponseInfoAPI {
@GET(Constant.LOGIN)
Call<ResponseInfo> login(@Query("username") String username, @Query("password") String password);
Call<ResponseInfo> getHomeInfo();
}
1.5. BasePresenter
1.5.1. 作用
根据Mvp模式,P处理了页面主要业务逻辑。而常用的app都是联网项目,一般只有本地数据库与网络请求两种数据。
所以可以将retrofit工具类与ormlite工具类都写在BasePresenter里面,以后处理数据就只要做以下两点即可
l 继承BasePresenter
l 覆盖重写显示方法与出错方法
com.itheima.takeout.presenter.BasePresenter
/**
* 通用的处理业务操作
*/
public abstract classBasePresenter {
publicBasePresenter() {
initRetrofit();
initOrmlite();
}
1.5.2. 初始化retrofit
// 联网工作的管理和数据库管理
// 联网
protectedRetrofitretrofit;
protected ResponseInfoAPIresponseInfoAPI;
// 数据库
protectedDBHelperhelper;
private voidinitRetrofit() {
retrofit= new Retrofit.Builder().
baseUrl(Constant.HOME).//配置主机地址
addConverterFactory(GsonConverterFactory.create()).//配置json的解析器
build();//创建
responseInfoAPI= retrofit.create(ResponseInfoAPI.class);//反射读取接口上的变量
}
private voidinitOrmlite() {
// 获取上下文
// 问题:如果上下文对应的是某个Activity或Fragment,生命周期过短
// 此处设置的上下文需要有较长的生命周期
helper= DBHelper.getInstance(MyApplication.getInstance());
}
1.5.3. 正确与错误的回调处理
当获取到服务器返回数据后会出发设置好的Callback,两个方法如下:
public voidonResponse(Call<ResponseInfo> call, Response<ResponseInfo> response)
public void onFailure(Call<ResponseInfo> call, Throwable t)
我们需要对回复的结果做进一步处理,首先必须要判断code值,如果为0表示当前请求操作服务器处理成功,返回用户想要数据,如果不为0表示服务器处理该请求出现问题,比如:用户名或密码输入错误。这个信息我们需要统一展示给用户。所以我们需要对两个方法进行统一处理。在onResponse中需要
ResponseInfo body = response.body();
if("0".equals(body.getCode())) {
// 服务器处理成功,可以解析data数据了
parseDestInfo(body.getData());
}else{
String error=errorInfo.get(body.getCode());
onFailure(call,newRuntimeException(error));
}
如果出现服务器处理错误会出发onFailure方法,同时由于网络问题也会触发该方法,我们需要对出发来源进行区分,可以定义一个自己的异常,封装服务器返回错误提示信息,展示给用户,如果是网络问题则提示:请检查网络,或服务器忙等。代码如下(这里使用了RuntimeException)
//回调处理
protected classCallbackAdapterimplements Callback<ResponseInfo> {
privateHashMap<String, String>errorInfo;
publicCallbackAdapter() {
errorInfo= new HashMap<>();
errorInfo.put("5","");
}
@Override
public voidonResponse(Call<ResponseInfo> call, Response<ResponseInfo> response) {
ResponseInfo body = response.body();
if("0".equals(body.code)) {
// 服务器处理成功,可以解析data数据了
parseDestInfo(body.data);
} else {
String error =errorInfo.get(body.data);
onFailure(call,new RuntimeException(error));
}
}
@Override
public voidonFailure(Call<ResponseInfo> call, Throwable t) {
// 我们该如何区分异常,其他类型异常(如:网络有问题) 和 服务器处理请求失败的异常(如:登陆时,输入的用户名密码有误)
// 我们需要创建一个自己的异常类(MyException,偷懒的话使用RuntimeException),当服务器处理失败时,通过该异常包装显示数据
if(t instanceof RuntimeException) {
showError(((RuntimeException) t).getMessage());
} else {
showError("服务器忙,请稍后重试……");
}
}
}
/**
* 服务器处理失败时需要将错误信息提示给用户(如:用户名密码有错误)*
* @parammessage
*/
protected abstract voidshowError(String message);
/**
* 解析服务器回复数据
* @paramdata
*/
protected abstract voidparseDestInfo(String data);
2. 首页UI搭建
2.1. 运行效果
要点:选择器与点击切换页面
2.2. selectorChapek选择器插件-提高开发效率
命名规则可以查询提供的官方文档。
操作截图:
有特殊要求
l 需要drawable开头下的图片才能生效。
l 需要以_normal,_disabled结尾。可以参考以下配置表
2.3. 布局主页UI
参考布局
layout/activity_main.xml
<?xml version="1.0"encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.itheima.app_.MainActivity">
<FrameLayout
android:id="@+id/main_fragment_container"
android:background="#FFF000"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"></FrameLayout>
<LinearLayout
android:id="@+id/main_fragment_navi"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<!--首页-->
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="30dp"
android:src="@drawable/home"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom|center_horizontal"
android:text="首页"
android:textColor="@color/main_bottom_tv_color"/>
</FrameLayout>
<!--订单-->
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="30dp"
android:src="@drawable/order"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom|center_horizontal"
android:text="订单"
android:textColor="@color/main_bottom_tv_color"/>
</FrameLayout>
<!--个人-->
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="30dp"
android:src="@drawable/me"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom|center_horizontal"
android:text="个人"
android:textColor="@color/main_bottom_tv_color"/>
</FrameLayout>
<!--更多-->
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:layout_width="match_parent"
android:layout_height="30dp"
android:src="@drawable/more"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom|center_horizontal"
android:text="更多"
android:textColor="@color/main_bottom_tv_color"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>
2.4. 布局完成后,完成切换逻辑
主要有两点逻辑
l 进入页面默认显示的是主页高亮
l 用户通过点击按钮,被点中的高亮,未被点中的失去高亮
运行效果
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
//初始化 查找指定元素进行选中
currentTab= 0;
setTabSelected();
//添加事件
initListener();
}
private voidinitListener() {
//获取子元素个数
intchildCount =mainFragmentNavi.getChildCount();
for(inti = 0; i < childCount; i++) {
//获取子元素
FrameLayout childAt = (FrameLayout)mainFragmentNavi.getChildAt(i);
final intindex=i;
childAt.setOnClickListener(newView.OnClickListener() {
@Override
public voidonClick(View v) {
currentTab=index;
setTabSelected();//修改导航按钮的选中状态
}
});
}
}
//设置指定的导航按钮高亮
private voidsetTabSelected() {
intchildCount =mainFragmentNavi.getChildCount();
for(inti = 0; i < childCount; i++) {
FrameLayout childAt = (FrameLayout)mainFragmentNavi.getChildAt(i);
ImageView image = (ImageView) childAt.getChildAt(0);//图片
TextView text = (TextView) childAt.getChildAt(1);//文字
//设置选中
if(currentTab== i) {
image.setEnabled(false);
text.setEnabled(false);
} else {
image.setEnabled(true);
text.setEnabled(true);
}
}
}
重要方法
viewgroup.getChildCount()获取布局包含的子元素个数
viewgroup.getChildAt(i)获取指定下标的子元素
view.setEnabled(true);设置选择器效果
2.5. 按钮完成后,创建四个Fragment
l 布局fragment UI
如图
layout/fragment_home.xml
layout/fragment_more.xml
layout/fragment_order.xml
layout/fragment_user.xml
布局内容现在只放一个TextView即可
如下
<?xml version="1.0"encoding="utf-8"?>
<FrameLayoutxmlns: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"
>
<TextView
android:gravity="center"
android:layout_width="match_parent"
android:text="个人"
android:layout_height="match_parent"/>
</FrameLayout>
l 创建对应的Fragment(项目建议使用有意义的命名,不用AFragment,BFragment这样的命名)
com.itheima.app_.ui.fragment.HomeFragment
com.itheima.app_.ui.fragment.MoreFragment
com.itheima.app_.ui.fragment.OrderFragment
com.itheima.app_.ui.fragment.UserFragment
每个Fragment只是简单地打气进布局,现在还没有编写逻辑
public classHomeFragmentextends BaseFragment {
@Nullable
@Override
publicView onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {
returninflater.inflate(R.layout.fragment_home,null);
}
}
l 补充底部按钮切换时,同步Fragment切换
//在onCreate里面调用而且在setTabSelected方法之前
private voidinitFragments() {
mFragments= new Fragment[]{newHomeFragment(),newOrderFragment(),newUserFragment(),newMoreFragment()};
}
//设置指定的导航按钮高亮
private voidsetTabSelected() {
intchildCount =mainFragmentNavi.getChildCount();
for(inti = 0; i < childCount; i++) {
FrameLayout childAt = (FrameLayout)mainFragmentNavi.getChildAt(i);
ImageView image = (ImageView) childAt.getChildAt(0);//图片
TextView text = (TextView) childAt.getChildAt(1);//文字
//设置选中
if(currentTab== i) {
image.setEnabled(false);
text.setEnabled(false);
//切换Fragment
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.main_fragment_container,mFragments[currentTab]);
transaction.commit();
} else {
image.setEnabled(true);
text.setEnabled(true);
}
}
}
3. 首页标题栏沉浸式效果
3.1. 实现方法一:开源项目
android4.4开始实现了状态栏的沉浸,即状态栏一体化,效果如图:
简单地说可以通过 一些特殊配置将页面内容伸入到标题栏底部,或者将标题栏背景着色。
[开源库]https://github.com/jgilfelt/SystemBarTint
l 先依赖
//状态栏
compile 'com.readystatesoftware.systembartint:systembartint:1.0.3'
依赖后只有一个类
l 使用
>1.布局添加
android:fitsSystemWindows="true"
android:clipToPadding="false"
l android:fitsSystemWindows="true"
作用就是你的contentview是否忽略actionbar,title,屏幕的底部虚拟按键,将整个屏幕当作可用的空间。
正常情况,contentview可用的空间是去除了actionbar,title,底部按键的空间后剩余的可用区域;这个属性设置为true,则忽略,false则不忽略
l android:clipToPadding="false"
clipToPadding:控件的绘制区域是否在padding里面, 值为true时padding那么绘制的区域就不包括padding区域;
>调用着色代码
private voidinitSystemBar() {
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.KITKAT) {
Window win = getWindow();
WindowManager.LayoutParams winParams = win.getAttributes();
//修改window的综合属性flags
//WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS含义为状态栏透明
winParams.flags|= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
win.setAttributes(winParams);
}
//调用开源库SystemBarTintManager进行状态栏着色 产生沉浸式效果
SystemBarTintManager tintManager =new SystemBarTintManager(this);
tintManager.setStatusBarTintEnabled(true);//使用状态栏着色可用
tintManager.setStatusBarTintColor(Color.GREEN);//指定颜色进行着色
}
3.2. 实现方法二:values-v21(过时)
需要处理4.4以下版本、4.4、5.0以上版本,创建:values-v19,values-v21
Values:
<stylename="AppTheme"parent="Theme.AppCompat.Light.NoActionBar">
</style>
values-v19:
<stylename="AppTheme"parent="Theme.AppCompat.Light.NoActionBar">
<itemname="android:windowTranslucentStatus">true</item>
<itemname="android:windowTranslucentNavigation">true</item>
</style>
values-v21:
<stylename="AppTheme"parent="Theme.AppCompat.Light.NoActionBar">
<itemname="android:windowTranslucentStatus">false</item>
<itemname="android:windowTranslucentNavigation">true</item>
<!--Android 5.x开始需要把颜色设置透明,否则导航栏会呈现系统默认的浅灰色-->
<itemname="android:statusBarColor">@android:color/transparent</item>
</style>
l 注意问题
>0.写死根据标签缩进,显示出状态栏。
>1.获取状栏高度,调整根据布局的padding
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RelativeLayout activity_main= (RelativeLayout) findViewById(R.id.activity_main);
inttop=getStatusBarHeight();
activity_main.setPadding(0,top,0,0);
}
/**
* 获取状态栏的高度
* @return
*/
protected intgetStatusBarHeight(){
try
{
Class<?> c=Class.forName("com.android.internal.R$dimen");
Object obj=c.newInstance();
Field field=c.getField("status_bar_height");
intx=Integer.parseInt(field.get(obj).toString());
return getResources().getDimensionPixelSize(x);
}catch(Exception e){
e.printStackTrace();
}
return0;
}
4. 首页-argb标题栏渐变
运行效果
>布局标题(这里为了讲清原理先使用TextView代替,项目中现在流行ToolBar)
layout/activity_main.xml
<?xml version="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.itheima.app3.MainActivity">
<TextView
android:text="我是标题"
android:id="@+id/toolbar"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#3AB2FF">
</TextView>
</RelativeLayout>
>代码获取渐变颜色Color.argb
public classMainActivityextends AppCompatActivity {
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
finalTextView toolbar = (TextView) findViewById(R.id.toolbar);
toolbar.setOnClickListener(newView.OnClickListener() {
floatscale =0.5f;
@Override
public voidonClick(View v) {
//透明度渐变
scale+= 0.05;
if(scale>= 1) {
scale= 0.5f;
}
floatalpha = scale *255;
toolbar.setText("alpah="+ alpha);
toolbar.setBackgroundColor(Color.argb((int) alpha, 57,174,255));
}
});
}
}
重要方法Color.argb(源代码分析)
其它rgb可以从拾色器取到
5. 首页-recyclerView
5.1. Rv基本使用
l 运行效果
>1.依赖(rv是一个兼容包的控件,想使用这个控件必须先加依赖)
compile'com.android.support:recyclerview-v7:24.2.1'
>2.布局rv
layout/activity_main.xml
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
>3.查找rv并使用Adapter初始化
privateList<String>list;
private voidinitRv() {
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv);
//设置布局管理者(本质是告诉rv怎么排列item)
recyclerView.setLayoutManager(newLinearLayoutManager(this));
getData();//模拟获取商品数据
MyAdapter myAdapter =new MyAdapter(list);
recyclerView.setAdapter(myAdapter);
}
public voidgetData() {
list= new ArrayList<>();
for(inti = 0; i <30; i++) {
list.add("商品记录...."+ i);
}
}
>4.适配器创建(ListView.BaseAdapter是四个方法而rv.Adapter是三个方法)
com.itheima.app3.MyAdapter
public classMyAdapterextends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
privateList<String>mData;
publicMyAdapter(List<String> list) {
mData= list;
}
//rv通过该方法获取item控件的缓存
@Override
publicMyViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
intitem = R.layout.item;
View view = LayoutInflater.from(parent.getContext()).inflate(item, parent, false);
MyViewHolder hd=newMyViewHolder(view);
returnhd;
}
//rv通过该方法给缓存控件赋值
@Override
public voidonBindViewHolder(MyViewHolder holder,int position) {
holder.text.setText(mData.get(position));
}
//rv通过该方法知道item个数
@Override
public intgetItemCount() {
returnmData.size();
}
static classMyViewHolderextends RecyclerView.ViewHolder{
@InjectView(R.id.img)
ImageViewimg;
@InjectView(R.id.text)
TextView text;
MyViewHolder(View view) {
super(view);
ButterKnife.inject(this, view);
}
}
}
>5.补充下item布局(这个每个项目的不同页面布局是不一样的。这里使用最简单的布局ImageView+TextView)
layout/item.xml
<?xml version="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context="com.itheima.app3.MainActivity">
<ImageView
android:id="@+id/img"
android:layout_margin="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/img"
android:text="我是标题"></TextView>
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#C80000"/>
</RelativeLayout>
5.2. Rv进阶(滑动引起标题渐变)
l 运行效果
>1.添加rv的滚动事件监听器
//添加滚动事件
recyclerView.addOnScrollListener(listener);
}
@Override
protected voidonDestroy() {
super.onDestroy();
//在页面销毁后,因为控件也被销毁了,所以把监听器移除。
recyclerView.removeOnScrollListener(listener);
}
>2.计算滑动距离与透明度值
privateRecyclerView.OnScrollListenerlistener = newRecyclerView.OnScrollListener() {
//滑动状态有三种。SCROLL_STATE_IDLE 滑动停止,SCROLL_STATE_DRAGGING 手指未离开,SCROLL_STATE_SETTLING自动滚动
@Override
public voidonScrollStateChanged(RecyclerView recyclerView,int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch(newState) {
caseRecyclerView.SCROLL_STATE_DRAGGING:
System.out.println("SCROLL_STATE_DRAGGING");
break;
caseRecyclerView.SCROLL_STATE_SETTLING:
System.out.println("SCROLL_STATE_SETTLING");
break;
caseRecyclerView.SCROLL_STATE_IDLE:
System.out.println("SCROLL_STATE_IDLE");
break;
}
}
//滑动距离累计值
private intmDistanceY=0;
// dy : 垂直滚动距离
// dy > 0时为手指向上滚动, 列表滚动显示下面的内容
// dy < 0时为手指向下滚动, 列表滚动显示上面的内容
@Override
public voidonScrolled(RecyclerView recyclerView,int dx, intdy) {
super.onScrolled(recyclerView, dx, dy);
mDistanceY+=dy;
System.out.println("distanceY="+mDistanceY);
//滑动的距离
//toolbar的高度
inttoolbarHeight =mToolbar.getBottom();
//当滑动的距离 <= toolbar高度的时候,改变Toolbar背景色的透明度,达到渐变的效果
if(mDistanceY<= toolbarHeight) {
floatscale = (float)mDistanceY/ toolbarHeight;
floatalpha = scale *255;
mToolbar.setBackgroundColor(Color.argb((int) alpha, 57,174,255));
} else {
//滑动距离超过标题栏就设置成完全不透明
mToolbar.setBackgroundColor(Color.argb((int)255,57,174,255));
}
}
};
重点:
l 监听器的回调方法
l 三种状态
l Int dy变量的含义 是一个滑动值而不是一个累加值
5.3. Rv添加头部
l 运行结果
分析:ListView有一个可以添加头部的方法listview.addHeadView(view),但是rv是没有这样的方法的。
我们可以换思路,添加两种条目,一种显示位置为0,为headview.另一种显示位置为1...n
为gooditem.这样其实就是实现复杂列表的思路。
>1.替换adapter
这个地方大家可能有些奇怪,Adapter还没创建出来怎么就使用了?
面向对象:就是先使用后创建的思路。
// MyAdapter myAdapter = new MyAdapter(list);
MultiTypeAdapter myAdapter=newMultiTypeAdapter(list);
recyclerView.setAdapter(myAdapter);
>2.布局两个item
layout/item.xml
<?xml version="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context="com.itheima.app3.MainActivity">
<ImageView
android:id="@+id/img"
android:layout_margin="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@id/img"
android:text="我是标题"></TextView>
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:background="#C80000"/>
</RelativeLayout>
layout/head.xml
<?xml version="1.0"encoding="utf-8"?>
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFF000">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="这里面是头部可添加各种控件"/>
</RelativeLayout>
>3.编写MultiTypeAdapter
com.itheima.app3.MultiTypeAdapter
public classMultiTypeAdapterextends RecyclerView.Adapter<RecyclerView.ViewHolder> {
privateList<String>mData;
publicMultiTypeAdapter(List<String> list) {
mData= list;
}
//rv通过该方法获取item个数(此时 有两种item 一种是 head,另外的是商品item)
@Override
public intgetItemCount() {
return1 +mData.size();
}
//rv通过该方法知道position与视图的关系
public static final intITEM_HEAD = 0;
public static final intITEM_GOOD = 1;
//只有第一项是头部 其它都是商品
@Override
public intgetItemViewType(intposition) {
if(position ==0) {
returnITEM_HEAD;
} else {
returnITEM_GOOD;
}
}
static classHeadViewHolderextends RecyclerView.ViewHolder {
@InjectView(R.id.text)
TextView text;
publicHeadViewHolder(View view) {
super(view);
ButterKnife.inject(this, view);
}
}
static classGoodViewHolderextends RecyclerView.ViewHolder {
@InjectView(R.id.img)
ImageViewimg;
@InjectView(R.id.text)
TextView text;
publicGoodViewHolder(View view) {
super(view);
ButterKnife.inject(this, view);
}
}
//rv通过该方法获取item对应的缓存holder
@Override
publicRecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
if(viewType ==ITEM_HEAD) {
View head = View.inflate(parent.getContext(), R.layout.head,null);
HeadViewHolder hd =new HeadViewHolder(head);
returnhd;
} else {
View goodView = View.inflate(parent.getContext(), R.layout.item,null);
GoodViewHolder hd =new GoodViewHolder(goodView);
returnhd;
}
}
@Override
public voidonBindViewHolder(RecyclerView.ViewHolder holder,int position) {
//判断显示
intitemViewType = getItemViewType(position);
if(itemViewType ==ITEM_HEAD) {
HeadViewHolder hd = (HeadViewHolder) holder;
hd.text.setText("我是头部等待获取网络数据");
} else {
GoodViewHolder hd = (GoodViewHolder) holder;
String item =mData.get(position -1);
hd.text.setText(item);
}
}
}
注意跟普通adapter差别
l 多种item视图
l 多种holder用来缓存item视图
l 必须有一个方法告诉rv怎么排列这些holder即getItemViewType
l 根据该方法getItemViewType决定返回holder与显示holder
6. 再掌握一个轮播大图SliderLayout
l 运行效果
[开源控件]https://github.com/daimajia/AndroidImageSlider
使用方法
>1.配置权限
<uses-permissionandroid:name="android.permission.INTERNET"/>
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
>2.依赖开源库
compile'com.squareup.picasso:picasso:2.3.2'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.daimajia.slider:library:1.1.5@aar'
>3.布局显示控件
<com.daimajia.slider.library.SliderLayout
android:id="@+id/slider"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
>4.初始化与优化
sliderLayout= (SliderLayout) findViewById(R.id.slider);
//添加图片控件
for(inti=0;i<mTitles.length;i++)
{
TextSliderView textSliderView=newTextSliderView(this);
textSliderView.description(mTitles[i]);//设置标题
textSliderView.image(mImages[i]);//设置图片的网络地址
textSliderView.setScaleType(BaseSliderView.ScaleType.CenterCrop);//设置图片的缩放效果;
//添加到布局中显示
sliderLayout.addSlider(textSliderView);
}
//设置指示器的位置
sliderLayout.setPresetIndicator(SliderLayout.PresetIndicators.Center_Bottom);
//设置图片的切换效果
sliderLayout.setPresetTransformer(SliderLayout.Transformer.Accordion);
// sliderLayout.setCustomAnimation(new DescriptionAnimation()); 添加textView动画特效
//设置切换时长2000 ,时长越小,切换速度越快
sliderLayout.setDuration(2000);
}
//性能优化。当页面显示时进行自动播放
@Override
protected voidonStart() {
super.onStart();
sliderLayout.startAutoCycle();
}
//性能优化。当页面不显示时暂停自动播放
@Override
protected voidonStop() {
super.onStop();
sliderLayout.stopAutoCycle();
}
如果需要自定义指示器可以参考示例(不要求记忆)
[地址]https://github.com/daimajia/AndroidImageSlider/wiki/Custom-Indicators
7. 掌握rv,显示首页数据
分析:任何一个页面都是显示数据的,此处首页显示的是服务端首页接口的数据。
只要把数据获取到本地,并且成功解析后就可以编写各种显示逻辑
7.1. 先发请求获取数据
>1编写url地址常量
com.itheima.app_.utils.Contants
public classContants {
public static final StringHOME="home";
public static final StringBASE_URL="http://10.0.2.2:8080/TakeoutServer/";
}
>2.编写返回数据javaBean(在as中可以通过工具GsonFormat生成)
com.itheima.app_.model.net.ResponseData
public classResponseData {
publicString code;
publicString data;
}
>3.编写retrofit的请求方法
com.itheima.app_.model.net.TakeOutApi
public interfaceTakeOutApi {
//http://192.168.0.107:8080/TakeoutServer/home
@GET(Contants.HOME)
Call<ResponseData> getHome();
}
>4.初始化retrofit框架
com.itheima.app_.model.net.HttpUtils
public classHttpUtils {
private staticRetrofitbuild;
private staticTakeOutApiapi;
public staticTakeOutApi getApi() {
if(build== null) {
build= newRetrofit.Builder().baseUrl(Contants.BASE_URL) //配置主机地址
.addConverterFactory(GsonConverterFactory.create(newGson()))//配置解析json框架
.build();
api= build.create(TakeOutApi.class);
}
returnapi;
}
}
>5.项目中使用更简单的CallBack封装SimpleCallBack
com.itheima.app_.model.net.SimpleCallBack
public classSimpleCallBackimplements Callback<ResponseData> {
@Override
public voidonResponse(Call<ResponseData> call, Response<ResponseData> response) {
if(response.body() !=null) {
ResponseData responseData = response.body();
String json = responseData.data;
// HomeData homeData = new Gson().fromJson(json, HomeData.class);
// System.out.println(homeData);
//填充到rv上面
showData(json);
} else {
//提示获取数据出错
showError(response.code(),new RuntimeException("数据出错"));
}
}
protected voidshowData(String json) {
//do nothing
}
@Override
public voidonFailure(Call<ResponseData> call, Throwable t) {
t.printStackTrace();
//提示获取数据出错
showError(-1,new RuntimeException(t));
}
protected voidshowError(inti, RuntimeException e) {
//do nothing
}
}
>6.有了以上内容即可编写mvp开发最重要的p
com.itheima.app_.presenter.HomeFragmentPresenter
public classHomeFragmentPresenter {
public voidgetData() {
//显示加载中
Call<ResponseData> call = HttpUtils.getApi().getHome();
SimpleCallBack callBack =new SimpleCallBack() {
@Override
protected voidshowData(String json) {
super.showData(json);
HomeData data =new Gson().fromJson(json, HomeData.class);
mView.showData(data);
//关闭加载
}
@Override
protected voidshowError(inti, RuntimeException e) {
super.showError(i, e);
//关闭加载
}
};
call.enqueue(callBack);
}
}
>7.使用Android4Junit新框架进行测试(项目中对核心数据有测试要求)
com.itheima.app_.HomeFragmentPresenterTest
@RunWith(AndroidJUnit4.class)
public classHomeFragmentPresenterTest {
@Test
public voiduseAppContext()throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("com.itheima.app_", appContext.getPackageName());
HomeFragmentPresenter presenter=newHomeFragmentPresenter();
presenter.getData();
Thread.sleep(30000);
}
}
l 看到服务端正确返回数据才可以进一步进行代码编写(同学们一般不注意测试,程序一出错就没法往下运行)
7.2. 编写完HomeFragmentPresenter即可用dagger2注入到页面
l 如图
>1.使用@Module与@Provide创建工厂模式
com.itheima.app_.dagger.module.HomeFragmentModule
@Module
public classHomeFragmentModule {
@Provides
publicHomeFragmentPresenter providerPresneter() {
return newHomeFragmentPresenter();
}
}
>2.使用@Compoment创建注入器
com.itheima.app_.dagger.component.HomeFragmentComponent
@Component(modules = {HomeFragmentModule.class})
public interfaceHomeFragmentComponent {
public voidinject(HomeFragment fragment);
}
>3.点击运行才能生成Dagger代码
>4.找到页面调用注入方法对@Inject变量进行注入
@Inject
HomeFragmentPresenterpresenter;
@Override
public voidonCreate(@NullableBundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerHomeFragmentComponent.builder()
.homeFragmentModule(newHomeFragmentModule())
.build()
.inject(this);//给当前页面带在@Inject变量进行注入
presenter.setView(this);//此时presenter不为空
}
7.3. 编写view的显示逻辑
com.itheima.app_.ui.fragment.HomeFragment
//显示加载
public voidshowLoading(booleanshow) {
loading.setVisibility(show ? View.VISIBLE: View.GONE);
}
//显示获取数据出错
public voidshowError(String message) {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
}
public voidshowData(HomeData data) {
//设置排列方式
rv.setLayoutManager(newLinearLayoutManager(getContext()));
//设置分割线
rv.addItemDecoration(newDividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL_LIST));
//设置适配器
HeaderAdapter adapter =new HeaderAdapter(data);
rv.setAdapter(adapter);
//添加标题滚动渐变效果
rv.addOnScrollListener(listener);
}
7.4. 完善presenter以view的调用
com.itheima.app_.presenter.HomeFragmentPresenter
public classHomeFragmentPresenter {
privateHomeFragmentmView;
public voidsetView(BaseFragment view) {
mView= (HomeFragment) view;
}
public voidresetView() {
mView= null;
}
public voidgetData() {
mView.showLoading(true);
Call<ResponseData> call = HttpUtils.getApi().getHome();
SimpleCallBack callBack =new SimpleCallBack() {
@Override
protected voidshowData(String json) {
super.showData(json);
mView.showLoading(false);
HomeData data =new Gson().fromJson(json, HomeData.class);
mView.showData(data);
}
@Override
protected voidshowError(inti, RuntimeException e) {
super.showError(i, e);
mView.showError(e.getMessage());
mView.showLoading(false);
}
};
call.enqueue(callBack);
}
}
7.5. 做好view的内存释放
presenter.setView(this);
}
@Override
public voidonDestroy() {
super.onDestroy();
presenter.resetView();
}
7.6. 创建带有headView的适配器
>1.布局三个部分的UI
layout/item_head.xml
layout/item_recomend.xml
layout/item_seller.xml
>2.创建三个部分的Holder
l 以下代码可以使用butterknife插件自动生成
com.itheima.app_.ui.holder.HeadViewHolder
public classHeadViewHolder extendsRecyclerView.ViewHolder {
@InjectView(R.id.slider)
SliderLayout slider;
publicHeadViewHolder(View view) {
super(view);
ButterKnife.inject(this, view);
}
}
com.itheima.app_.ui.holder.RecommendViewHolder
public classRecommendViewHolderextends RecyclerView.ViewHolder {
@InjectView(R.id.tv_division_title)
TextView tvDivisionTitle;
@InjectViews({R.id.text1,R.id.text2,R.id.text3,R.id.text4,R.id.text5,R.id.text6})
List<TextView>list;
publicRecommendViewHolder(View itemView) {
super(itemView);
ButterKnife.inject(this,itemView);
}
}
com.itheima.app_.ui.holder.SellerViewHolder
public classSellerViewHolder extendsRecyclerView.ViewHolder {
@InjectView(R.id.tvCount)
TextView tvCount;
@InjectView(R.id.image)
ImageView image;
@InjectView(R.id.tv_title)
TextView tvTitle;
@InjectView(R.id.ratingBar)
RatingBar ratingBar;
publicSellerViewHolder(View view) {
super(view);
ButterKnife.inject(this, view);
}
}
>3编写显示逻辑
com.itheima.app_.ui.adapter.HeaderAdapter
创建出数量对应的holder与位置正确的排序
public classHeaderAdapterextends RecyclerView.Adapter<RecyclerView.ViewHolder> {
publicHomeDatamData;
publicHeaderAdapter(HomeData data) {
mData= data;
}
@Override
public intgetItemCount() {
return1 +mData.body.size();
}
private final intITEM_HEAD =0;
private final intITEM_BODY =1;
private final intITEM_RECOMEND = 2;
@Override
public intgetItemViewType(intposition) {
if(position ==0) {
returnITEM_HEAD;
} else {
HomeData.BodyInfo bodyInfo =mData.body.get(position - 1);
if(1== bodyInfo.type) {
//显示推荐
returnITEM_RECOMEND;
} else {
returnITEM_BODY;
}
}
}
@Override
publicRecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,int viewType) {
if(viewType ==ITEM_HEAD) {
View itemHead = View.inflate(parent.getContext(), R.layout.item_head,null);
HeadViewHolder hd =new HeadViewHolder(itemHead);
returnhd;
} else if (viewType ==ITEM_RECOMEND) {
View itemRecommend = View.inflate(parent.getContext(), R.layout.item_recomend,null);
RecommendViewHolder hd =new RecommendViewHolder(itemRecommend);
returnhd;
} else {
View seller = View.inflate(parent.getContext(), R.layout.item_seller,null);
SellerViewHolder hd =new SellerViewHolder(seller);
returnhd;
}
}
getItemViewType里的判断逻辑对应于服务端返回的数据
>4.编写赋值逻辑
@Override
public voidonBindViewHolder(RecyclerView.ViewHolder holder,int position) {
intviewType = getItemViewType(position);
if(viewType ==ITEM_HEAD) {
HeadViewHolder hd = (HeadViewHolder) holder;
//显示头部数据
HomeData.HeadInfo head =mData.head;
//显示轮播大图
List<HomeData.HeadInfo.PromotionListInfo> pics = head.promotionList;
if(hd.slider.getTag() ==null) {
for(HomeData.HeadInfo.PromotionListInfo item : pics) {
TextSliderView img =new TextSliderView(hd.slider.getContext());
img.description(item.info);//文字描述
img.image(item.pic.replace("http://10.0.2.2:8080/TakeoutService/", Contants.BASE_URL));//加载图片
hd.slider.addSlider(img);
}
hd.slider.setTag("0");
}
} else if(viewType ==ITEM_RECOMEND) {
//显示推荐
RecommendViewHolder hd = (RecommendViewHolder) holder;
HomeData.BodyInfo bodyInfo =mData.body.get(position-1);
List<String> list = bodyInfo.recommendInfos;
for(inti = 0; i < list.size(); i++) {
hd.list.get(i).setText(list.get(i));
}
} else{
SellerViewHolder hd = (SellerViewHolder) holder;
//显示商品数据
List<HomeData.BodyInfo> list =mData.body;
HomeData.BodyInfo item = list.get(position -1);
try{
hd.ratingBar.setRating(Float.parseFloat(item.seller.score));//评分
}catch (Exception e) {
hd.ratingBar.setRating(0f);
}
hd.tvTitle.setText(item.seller== null ?"--" : item.seller.name);//商家名称
if(item.seller!= null) {
String url = item.seller.pic.replace("http://10.0.2.2:8080/TakeoutService/", Contants.BASE_URL);
if(!TextUtils.isEmpty(url)) {
Picasso.with(hd.ratingBar.getContext()).load(url).into(hd.image);
} else {
//显示默认
hd.image.setImageResource(R.mipmap.item_kfc);
}
} else {
//显示默认
hd.image.setImageResource(R.mipmap.item_kfc);
}
}
}
l 注意
1)取数据的下标.因为head已经占掉0位置那么recommend与body类型从集合中取出来position要少1
即mData.body.get(position-1);
2)为什么初始化大图时要判断?因为onBindViewHolder经过多次执行为让大图的个数由于重复添加而增多,
本项目中实际数量只有3张。
if(hd.slider.getTag() ==null) {
for(HomeData.HeadInfo.PromotionListInfo item : pics) {
TextSliderView img =new TextSliderView(hd.slider.getContext());
img.description(item.info);//文字描述
img.image(item.pic.replace("http://10.0.2.2:8080/TakeoutService/", Contants.BASE_URL));//加载图片
hd.slider.addSlider(img);
}
hd.slider.setTag("0");
}
7.7. 完成显示后就添加滑动引起标题渐变处理(argb)
l 运行效果
//添加标题滚动渐变效果
rv.addOnScrollListener(listener);
}
@Override
public voidonDestroyView() {
super.onDestroyView();
rv.removeOnScrollListener(listener);
ButterKnife.reset(this);
}
private RecyclerView.OnScrollListenerlistener = newRecyclerView.OnScrollListener() {
@Override
public voidonScrollStateChanged(RecyclerView recyclerView,int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
//滑动距离累计值
private intmDistanceY = 0;
@Override
public voidonScrolled(RecyclerView recyclerView,int dx, intdy) {
//int dy为滑动增量
super.onScrolled(recyclerView, dx, dy);
mDistanceY+= dy;
//滑动距离未超过标题底部计算透明度
if(mDistanceY<= toolbar.getBottom()) {
floatscale = mDistanceY*1.00f/ toolbar.getBottom();
System.out.println(mDistanceY+" mDistanceY scale="+scale);
intargb = Color.argb((int) (255 * scale),58,178,255);
toolbar.setBackgroundColor(argb);
} else {//超过即为不透明
intargb = Color.argb(255, 58,178,255);
toolbar.setBackgroundColor(argb);
}
}
};