黑马外卖笔记(二)

本文详细介绍了如何搭建一个外卖应用的项目框架,包括项目分包、依赖配置、通用工具类、数据库ORMlite配置、网络层Retrofit集成、BasePresenter的使用以及首页UI的搭建,涉及到布局设计、滑动事件处理和标题栏渐变效果。通过这个过程,读者可以学习到Android开发中的关键技术和实践技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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里面,以后处理数据就只要做以下两点即可

继承BasePresenter

覆盖重写显示方法与出错方法

 

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选择器插件-提高开发效率

 

 

命名规则可以查询提供的官方文档。

操作截图:

 


 

有特殊要求

需要drawable开头下的图片才能生效。

需要以_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. 布局完成后,完成切换逻辑

主要有两点逻辑

进入页面默认显示的是主页高亮

用户通过点击按钮,被点中的高亮,未被点中的失去高亮

 

运行效果


 

@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

布局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>

 

 

创建对应的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);
    }
}

 

补充底部按钮切换时,同步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

先依赖

//状态栏
compile 'com.readystatesoftware.systembartint:systembartint:1.0.3'

依赖后只有一个类

 

使用

>1.布局添加

  android:fitsSystemWindows="true"

  android:clipToPadding="false"

 android:fitsSystemWindows="true"

作用就是你的contentview是否忽略actionbar,title,屏幕的底部虚拟按键,将整个屏幕当作可用的空间。

正常情况,contentview可用的空间是去除了actionbar,title,底部按键的空间后剩余的可用区域;这个属性设置为true,则忽略,false则不忽略

 

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.45.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>

 

注意问题

 

>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基本使用

运行效果

 

 

>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进阶(滑动引起标题渐变)

运行效果

 

>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));
        }
    }
};

 

重点:

监听器的回调方法

三种状态

Int dy变量的含义 是一个滑动值而不是一个累加值

 

5.3. Rv添加头部

运行结果

 

分析: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差别

多种item视图

多种holder用来缓存item视图

必须有一个方法告诉rv怎么排列这些holdergetItemViewType

根据该方法getItemViewType决定返回holder与显示holder

 

 

6. 再掌握一个轮播大图SliderLayout

运行效果

 

[开源控件]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);
    }
}

看到服务端正确返回数据才可以进一步进行代码编写(同学们一般不注意测试,程序一出错就没法往下运行)

 

7.2. 编写完HomeFragmentPresenter即可用dagger2注入到页面

如图


>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. 完善presenterview的调用

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

 

以下代码可以使用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);
        }

    }
}

 

注意

1)取数据的下标.因为head已经占掉0位置那么recommendbody类型从集合中取出来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

运行效果

 

 

    //添加标题滚动渐变效果
  
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);
        }
    }
};

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值