Android手机应用开发实战(一) | 展示王者荣耀英雄信息的APP

使用说明

系统

  1. 自定义了APP的图标

  2. 启动app查看到启动动画(图片)

    P.S. 可能会出现启动动画消失回到桌面的情况,等待后进入app

  3. 进入app默认显示主界面,底部标签为英雄、装备、铭文、技能

  4. 进入app后播放背景音乐

英雄主界面

  1. 默认显示:搜索框,工具框(ToolBar),添加英雄按钮,收藏英雄显示,英雄分类显示还有具体的英雄列表
  2. 点击搜索框出现候选英雄列表(不输入会默认显示所有英雄列表,输入文字后会匹配输入文本为前缀的英雄名),点击搜索按钮或者候选框中的英雄名称进入英雄详情,如果搜索框中的英雄名不存在会弹出Toast提示
  3. 这是一个图片轮换器,理论上会3秒切换一个英雄(显示英雄海报),且支持循环切换(最后一个英雄切换回最前面一个),在英雄详情页面会有收藏与取消收藏的操作,下面的小圆点会指示当前图片的位置(红色指示)
  4. 添加英雄的按钮,点击之后会跳到添加英雄的页面
  5. 英雄职业的分类显示,点击不同的按钮就会过滤出该职业的英雄,更新英雄列表(搜索框提示列表不受影响)
  6. 英雄列表,网格布局,一共五列,只显示英雄头像,点击会进入英雄详情
1543053765227
1543053897874

英雄详情

  1. 返回主界面按钮
  2. 当前页面名称(英雄)
  3. 更多操作(编辑,删除,保存)
    1. 点击编辑之后,所有可修改的内容都处于可编辑状态(默认是不可编辑状态)
    2. 点击保存之后才能保存内容的修改(包括收藏!收藏之后直接返回是无效的,需要点击保存)
    3. 点击删除按钮之后弹出确认对话框,确定之后删除该英雄并且返回主界面
  4. 英雄头像(可修改)
  5. 英雄名字(不可修改)
  6. 英雄称号(可修改)
  7. 英雄职业(可修改)
  8. 英雄海报(可修改)
  9. 收藏英雄或者取消收藏(只能点击,不能修改)
  10. 英雄生存能力,攻击伤害,技能效果,上手难度数值显示(可修改)
  11. 英雄技能图标按钮,点击查看不同的技能(包括被动只有四个技能,天美最近的英雄技能越来越多了……)(不可修改)
  12. 英雄技能具体描述(不可修改)
  13. 英雄推荐出装(不可修改),点击会跳转到相应的装备详情页面
detail_hero
1543055147962
1543055260034

添加英雄

  1. 返回主界面
  2. 更多操作
    1. 添加头像,弹出系统图片选择框选择图片作为英雄头像(默认是:王者荣耀图标)
    2. 添加语音,弹出系统语音选择框作为英雄语音(默认是:PentaKill语音,享受五杀的感觉!)
  3. 选择英雄海报(默认是:当前图片)
  4. 填写英雄名字(不可为空,不可重复)
  5. 填写英雄称号(默认为:王者小兵)
  6. 填写英雄职业(默认为:法师)
  7. 填写英雄生存能力,攻击伤害,技能效果,上手难度等信息(范围是1-9,默认是1)
1543055421948
1543055629975

导航栏(SmartTabLayout)

使用了GitHub上的这个UI

https://github.com/ogaclejapan/SmartTabLayout

1542817288428

所以activity_main.xml中只有一个ViewPager和一个导航栏,(其实导航栏是在上面)

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    />
<com.ogaclejapan.smarttablayout.SmartTabLayout
    android:background="@color/colorPrimaryDark"
    android:id="@+id/viewpagertab"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    app:stl_indicatorAlwaysInCenter="false"
    app:stl_indicatorWithoutPadding="false"
    app:stl_indicatorInFront="false"
    app:stl_indicatorInterpolation="smart"
    app:stl_indicatorGravity="bottom"
    app:stl_indicatorColor="#40C4FF"
    app:stl_indicatorThickness="4dp"
    app:stl_indicatorWidth="auto"
    app:stl_indicatorCornerRadius="2dp"
    app:stl_overlineColor="@color/colorPrimary"
    app:stl_overlineThickness="0dp"
    app:stl_underlineColor="#4D000000"
    app:stl_underlineThickness="1dp"
    app:stl_dividerColor="#4D000000"
    app:stl_dividerThickness="1dp"
    app:stl_defaultTabBackground="?attr/selectableItemBackground"
    app:stl_defaultTabTextAllCaps="true"
    app:stl_defaultTabTextColor="@color/white"
    app:stl_defaultTabTextSize="12sp"
    app:stl_defaultTabTextHorizontalPadding="16dp"
    app:stl_defaultTabTextMinWidth="0dp"
    app:stl_distributeEvenly="true"
    app:stl_clickable="true"
    app:stl_titleOffset="24dp"
    app:stl_drawDecorationAfterTab="false"
    />

MainActivity.java

FragmentPagerItemAdapter adapter = new FragmentPagerItemAdapter(
                getSupportFragmentManager(), FragmentPagerItems.with(this)
                .add(R.string.title1, Fragment1.class)
                .add(R.string.title2, Fragment2.class)
                .add(R.string.title3, Fragment3.class)
                .add(R.string.title4, Fragment4.class)
                .create());
        ViewPager viewPager = findViewById(R.id.viewpager);
        viewPager.setAdapter(adapter);
        SmartTabLayout viewPagerTab = findViewById(R.id.viewpagertab);
        viewPagerTab.setViewPager(viewPager);

这里的Fragment1,2,3,4分别是四个页面:英雄,装备,明文,技能

工具栏(ToolBar)

其实有两种ToolBar:

  • android.support.v7.widget.Toolbar
  • Toolbar

两个都能用,但是有一些区别

区别一ToolBar的就是MenuItem显示不出来图标,另一个可以

1542817668675

区别二android.support.v7.widget.Toolbar在设计页面没有预览,另一个有

1542817704898

区别三android.support.v7.widget.Toolbar如果是来自其他Activity跳转过来的,它会自动加上标题,而且会自动有返回按钮,而且添加的菜单栏不太好设计(具体我也忘了),所以除了主界面,其他界面我都是用Toolbar,方便自己定制

使用方法:

fragment1.xml

<android.support.v7.widget.Toolbar
    android:id="@+id/hero_toolbar"
    android:layout_width="match_parent"
    android:layout_height="45dp"
    android:background="@color/colorPrimaryDark">
</android.support.v7.widget.Toolbar>

Android视图下创建一个新的文件夹menu

1542872067478

新建hero_menu.xml

1542872087460

用来保存ToolBar上的菜单栏布局

1542817902374

这里只有一个菜单,表示添加英雄,而且图标用定义的图标

showAsAction表示是否展开到工具栏上

  • always表示总是展开
  • ifRoom表示如果有足够的空间就展开
  • never表示总是不展开
  • withText表示使菜单项和它的图标,菜单文本一起显示。

那么不展开的菜单会到哪里呢?
比如英雄详情页面,它会隐藏在更多按钮里(显示为三个点)

1542818274070
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_hero_add"
        android:title="添加英雄"
        android:icon="@mipmap/add"
        app:showAsAction="always" />
</menu>

然后需要在Fragment1.java文件中动态设置ToolBar的样式,以及设置点击Item响应的事件,这里只有一个事件,就是跳转到添加英雄页面

//获取ToolBar控件
android.support.v7.widget.Toolbar toolbar = view.findViewById(R.id.hero_toolbar);
toolbar.inflateMenu(R.menu.hero_menu);//设置右上角的填充菜单
toolbar.setOnMenuItemClickListener(new android.support.v7.widget.Toolbar.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem menuItem) {
        switch (menuItem.getItemId()) {
            //添加英雄
            case R.id.action_hero_add:
                Intent intent = new Intent(getActivity(), HeroAdd.class);
                intent.putStringArrayListExtra("hero_names", mAdapter.getAllNames());
                startActivityForResult(intent, 1);
                break;
        }
        return true;
    }
});

还有其他的菜单布局,待会会使用

hero_detail_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_hero_edit"
        android:title="编辑"
        android:icon="@mipmap/edit"
        app:showAsAction="never" />
    <item
        android:id="@+id/action_hero_save"
        android:title="保存"
        android:icon="@mipmap/save"
        app:showAsAction="never" />
    <item
        android:id="@+id/action_hero_delete"
        android:title="删除"
        android:icon="@mipmap/delete"
        app:showAsAction="never"/>
</menu>

hero_add_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_hero_add_icon"
        android:title="添加头像"
        app:showAsAction="never"/>
    <item
        android:id="@+id/action_hero_add_voice"
        android:title="添加语音"
        app:showAsAction="never"/>
    <item
        android:id="@+id/action_hero_add_save"
        android:title="保存英雄"
        android:icon="@mipmap/white_save"
        app:showAsAction="ifRoom" />
</menu>

java代码待会再讨论

英雄列表(RecyclerView)

英雄的实体类Hero.java

public class Hero implements Serializable{
    //保存的是image的uri
    //英雄海报
    private String image = "android.resource://com.team1.kingofhonor/" +R.mipmap.hero;
    //英雄语音
    private String voice = "android.resource://com.team1.kingofhonor/" +R.raw.pentakill;
    //英雄图标
    private String icon = "android.resource://com.team1.kingofhonor/" +R.mipmap.hero_icon;
    //英雄技能图标
    private String skill1_icon = "android.resource://com.team1.kingofhonor/" +R.mipmap.juyoujing1;
    private String skill2_icon = "android.resource://com.team1.kingofhonor/" +R.mipmap.juyoujing2;
    private String skill3_icon = "android.resource://com.team1.kingofhonor/" +R.mipmap.juyoujing3;
    private String skill4_icon = "android.resource://com.team1.kingofhonor/" +R.mipmap.juyoujing4;
    //英雄技能描述
    private String skill1_description = "技能1";
    private String skill2_description = "技能2";
    private String skill3_description = "技能3";
    private String skill4_description = "技能4";
    //英雄名字
    private String name = "名字未设置";
    //英雄称号
    private String alias = "王者小兵";
    //英雄职业
    private String category;//1.法师 2.刺客 3.射手 4.辅助 5.战士 6.坦克
    //推荐装备
    private String equip1 = "破军";
    private String equip2 = "破军";
    private String equip3 = "破军";
    private String equip4 = "破军";
    private String equip5 = "破军";
    private String equip6 = "破军";
    //生存能力
    private int viability = 1;
    //攻击伤害
    private int attack_damage= 1;
    //技能伤害
    private int skill_damage = 1;
    //上手难度
    private int difficulty = 1;

    private Boolean favorite = false;//收藏
    private Boolean deleted = false;//true表示已经删除
    private Boolean added = false;//true表示是新加的
    private Boolean modified = false;//true表示已经修改

    public Hero(){}
    ...
}

这里英雄的语音,头像,海报什么的资源文件全都是存储对应的Uri字符串

列表布局hero_list.xml

其实就显示了一个英雄头像

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="60dp"
android:layout_height="60dp"
    android:layout_margin="5dp"
xmlns:app="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/hero_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@mipmap/juyoujing_icon"/>


</android.support.constraint.ConstraintLayout>

页面视图fragment1.xml

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/hero_recyclerview"
    tools:listitem="@layout/hero_list"
    android:dividerHeight="10dp"
    android:layout_marginTop="290dp"
    android:layout_marginStart="10dp"
    android:layout_marginEnd="10dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent">
</android.support.v7.widget.RecyclerView>

列表适配器HeroAdapter.java

这里仅仅实现了基本的显示功能,后面还会添加新的方法

package com.team1.kingofhonor.adapter;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.team1.kingofhonor.R;
import com.team1.kingofhonor.model.Hero;
import com.team1.kingofhonor.sqlite.HeroSQLiteHelper;

import java.util.ArrayList;
import java.util.List;


public class HeroAdapter extends RecyclerView.Adapter<HeroAdapter.ViewHolder> {

    private static List<Hero> mDatas;
    private HeroAdapter.OnItemClickListener onItemClickListener;

    public HeroAdapter(List<Hero> list) {
        mDatas = list;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        // 实例化展示的view
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.hero_list, parent, false);
        // 实例化viewholder
        ViewHolder viewHolder = new ViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        // 绑定数据
        //holder.hero_name.setText(mDatas.get(position).getName());
        holder.hero_image.setImageURI(Uri.parse(mDatas.get(position).getIcon()));
        //listener
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });

        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemLongClick(holder.itemView, pos);
                }
                //表示此事件已经消费,不会触发单击事件
                return true;
            }
        });

    }

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

    public void addNewItem(Hero f) {
        if(mDatas == null) {
            mDatas = new ArrayList<>();
        }
        mDatas.add(0, f);
        notifyItemInserted(0);
    }

    public void deleteItem(int pos) {
        if(mDatas == null || mDatas.isEmpty()) {
            return;
        }
        mDatas.remove(pos);
        notifyItemRemoved(pos);
    }

    public Hero getItem(int pos){
        if(mDatas == null || mDatas.isEmpty())
            return null;
        return mDatas.get(pos);
    }
    /**
     * 设置回调监听
     *
     * @param listener
     */
    public void setOnItemClickListener(HeroAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }

    public static class ViewHolder extends RecyclerView.ViewHolder {
        TextView hero_name;
        ImageView hero_image;
        public ViewHolder(View itemView) {
            super(itemView);
            //hero_name = itemView.findViewById(R.id.hero_name);
            hero_image = itemView.findViewById(R.id.hero_image);
        }
    }
}

代码文件Fragment1.java

这里设置成网格显示,一共有5列

private View view;
private RecyclerView.LayoutManager mLayoutManager;
private RecyclerView mRecyclerView;
private HeroAdapter mAdapter;
...
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment1, container, false);
        //hero list
        mLayoutManager = new GridLayoutManager(getActivity(), 5);
        mAdapter = new HeroAdapter(mySQLiteHelper.getAllHeroes());
        mRecyclerView = view.findViewById(R.id.hero_recyclerview);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setAdapter(mAdapter);
        //设置英雄列表的边距
        //mRecyclerView.addItemDecoration(new SpaceItemDecoration(40));
        mAdapter.setOnItemClickListener(new HeroAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Intent intent = new Intent(getActivity(), HeroDetail.class);
                Bundle bundle=new Bundle();
                bundle.putSerializable("Click_Hero", mAdapter.getItem(position));
                intent.putExtras(bundle);
                clickPosition = position;
                startActivityForResult(intent, 0);
            }
            @Override
            public void onItemLongClick(View view, int position) {

            }
        });
}

英雄数据库(HeroSQLiteHelper)

HeroSQLiteHelper.java

除了实现基本的功能外,主要是加了个getAllHeroes方法,这是为了最开始得到数据库的数据给HeroAdapter赋初值

public class HeroSQLiteHelper extends SQLiteOpenHelper {
    // Database Version
    private static final int DATABASE_VERSION = 2;
    // Database Name
    private static final String DATABASE_NAME = "HeroDB";

    public HeroSQLiteHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        // SQL statement to create book table
        String CREATE_HERO_TABLE = "CREATE TABLE heroes ( " +
                "name TEXT PRIMARY KEY, " +
                "image TEXT," +
                "alias TEXT," +
                "category TEXT," +
                "viability INTEGER,"+
                "attack_damage INTEGER,"+
                "skill_damage INTEGER,"+
                "difficulty INTEGER,"+
                "voice TEXT," +
                "icon TEXT," +
                "favorite INTEGER," +
                "skill1_icon TEXT," +
                "skill1_description TEXT," +
                "skill2_icon TEXT," +
                "skill2_description TEXT," +
                "skill3_icon TEXT," +
                "skill3_description TEXT," +
                "skill4_icon TEXT," +
                "skill4_description TEXT)";

        // create books table
        db.execSQL(CREATE_HERO_TABLE);
        db.execSQL(CREATE_EQUIP_TABLE);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // Drop older  table if existed
        db.execSQL("DROP TABLE IF EXISTS heroes");
        this.onCreate(db);
    }
    //执行查询语句
    public Cursor query(String sql, String[] args)
    {
        SQLiteDatabase db = this.getReadableDatabase();
        Cursor cursor = db.rawQuery(sql, args);
        return cursor;
    }
    // table name
    private static final String TABLE_HEROES = "heroes";

    //Table Columns names
    private static final String KEY_NAME = "name";
    private static final String KEY_ALIAS = "alias";
    private static final String KEY_IMAGE = "image";
    private static final String KEY_CATEGORY = "CATEGORY";
    private static final String KEY_VIABILITY = "viability";
    private static final String KEY_ATTACK_DAMAGE = "attack_damage";
    private static final String KEY_SKILL_DAMAGE = "skill_damage";
    private static final String KEY_DIFFICULTY = "difficulty";
    private static final String KEY_VOICE = "voice";
    private static final String KEY_ICON = "icon";
    private static final String KEY_FAVORITE = "favorite";
    private static final String KEY_SKILL1_ICON = "skill1_icon";
    private static final String KEY_SKILL2_ICON = "skill2_icon";
    private static final String KEY_SKILL3_ICON = "skill3_icon";
    private static final String KEY_SKILL4_ICON = "skill4_icon";
    private static final String KEY_SKILL1_DESCRIPTION = "skill1_description";
    private static final String KEY_SKILL2_DESCRIPTION = "skill2_description";
    private static final String KEY_SKILL3_DESCRIPTION = "skill3_description";
    private static final String KEY_SKILL4_DESCRIPTION = "skill4_description";
    private static final String[] COLUMNS = {KEY_NAME,KEY_ALIAS,KEY_IMAGE,KEY_CATEGORY};

    public void addHero(Hero hero){
        Log.d("addHero", hero.toString());
        // 1. get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();
        // 2. create ContentValues to add key "column"/value
        ContentValues values = new ContentValues();
        values.put(KEY_NAME, hero.getName());
        values.put(KEY_ALIAS, hero.getAlias());
        values.put(KEY_IMAGE, hero.getImage());
        values.put(KEY_CATEGORY, hero.getCategory());
        values.put(KEY_VIABILITY, hero.getViability());
        values.put(KEY_ATTACK_DAMAGE, hero.getAttack_damage());
        values.put(KEY_SKILL_DAMAGE, hero.getSkill_damage());
        values.put(KEY_DIFFICULTY, hero.getDifficulty());
        values.put(KEY_VOICE, hero.getVoice());
        values.put(KEY_ICON, hero.getIcon());
        values.put(KEY_FAVORITE, hero.getFavorite());
        values.put(KEY_SKILL1_ICON, hero.getSkill1_icon());
        values.put(KEY_SKILL1_DESCRIPTION, hero.getSkill1_description());
        values.put(KEY_SKILL2_ICON, hero.getSkill2_icon());
        values.put(KEY_SKILL2_DESCRIPTION, hero.getSkill2_description());
        values.put(KEY_SKILL3_ICON, hero.getSkill3_icon());
        values.put(KEY_SKILL3_DESCRIPTION, hero.getSkill3_description());
        values.put(KEY_SKILL4_ICON, hero.getSkill4_icon());
        values.put(KEY_SKILL4_DESCRIPTION, hero.getSkill4_description());
        // 3. insert
        db.insert(TABLE_HEROES, // table
                null, //nullColumnHack
                values); // key/value -> keys = column names/ values = column values

        // 4. close
        db.close();
    }
    public Hero getHero(String name){
        // 1. get reference to readable DB
        SQLiteDatabase db = this.getReadableDatabase();
        // 2. build query
        Cursor cursor =
                db.query(TABLE_HEROES, // a. table
                        COLUMNS, // b. column names
                        " name = ?", // c. selections
                        new String[] { name }, // d. selections args
                        null, // e. group by
                        null, // f. having
                        null, // g. order by
                        null); // h. limit

        // 3. if we got results get the first one
        if (cursor != null)
            cursor.moveToFirst();
        // 4. build object
        Hero hero = new Hero();
        hero.setName(cursor.getString(0));
        hero.setImage(cursor.getString(1));
        hero.setAlias(cursor.getString(2));
        hero.setCategory(cursor.getString(3));
        hero.setViability(cursor.getInt(4));
        hero.setAttack_damage(cursor.getInt(5));
        hero.setSkill_damage(cursor.getInt(6));
        hero.setDifficulty(cursor.getInt(7));
        hero.setVoice(cursor.getString(8));
        hero.setIcon(cursor.getString(9));
        hero.setFavorite(cursor.getInt(10) == 1);
        hero.setSkill1_icon(cursor.getString(11));
        hero.setSkill1_description(cursor.getString(12));
        hero.setSkill2_icon(cursor.getString(13));
        hero.setSkill2_description(cursor.getString(14));
        hero.setSkill3_icon(cursor.getString(15));
        hero.setSkill3_description(cursor.getString(16));
        hero.setSkill4_icon(cursor.getString(17));
        hero.setSkill4_description(cursor.getString(18));
        // 5. return hero
        return hero;
    }
    // Get All Heroes
    public List<Hero> getAllHeroes() {
        List<Hero> heroes = new LinkedList<Hero>();
        // 1. build the query
        String query = "SELECT  * FROM " + TABLE_HEROES;
        // 2. get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();
        Cursor cursor = db.rawQuery(query, null);
        // 3. go over each row, build book and add it to list
        Hero hero = null;
        if (cursor.moveToFirst()) {
            do {
                hero = new Hero();
                hero.setName(cursor.getString(0));
                hero.setImage(cursor.getString(1));
                hero.setAlias(cursor.getString(2));
                hero.setCategory(cursor.getString(3));
                hero.setViability(cursor.getInt(4));
                hero.setAttack_damage(cursor.getInt(5));
                hero.setSkill_damage(cursor.getInt(6));
                hero.setDifficulty(cursor.getInt(7));
                hero.setVoice(cursor.getString(8));
                hero.setIcon(cursor.getString(9));
                hero.setFavorite(cursor.getInt(10) == 1);
                hero.setSkill1_icon(cursor.getString(11));
                hero.setSkill1_description(cursor.getString(12));
                hero.setSkill2_icon(cursor.getString(13));
                hero.setSkill2_description(cursor.getString(14));
                hero.setSkill3_icon(cursor.getString(15));
                hero.setSkill3_description(cursor.getString(16));
                hero.setSkill4_icon(cursor.getString(17));
                hero.setSkill4_description(cursor.getString(18));
                heroes.add(hero);
            } while (cursor.moveToNext());
        }
        // return heros
        return heroes;
    }

    public void deleteAllHeroes(){
        // 1. get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();
        // 2. delete
        db.delete(TABLE_HEROES,null,null);
        // 3. close
        db.close();
    }
    // Updating single book
    public int updateHero(Hero hero) {
        // 1. get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();
        // 2. create ContentValues to add key "column"/value
        ContentValues values = new ContentValues();
        values.put("name", hero.getName());
        values.put("image", hero.getImage().toString());
        values.put("alias", hero.getAlias());
        values.put("category", hero.getCategory());
        values.put(KEY_VIABILITY, hero.getViability());
        values.put(KEY_ATTACK_DAMAGE, hero.getAttack_damage());
        values.put(KEY_SKILL_DAMAGE, hero.getSkill_damage());
        values.put(KEY_DIFFICULTY, hero.getDifficulty());
        values.put(KEY_VOICE, hero.getVoice());
        values.put(KEY_ICON, hero.getIcon());
        values.put(KEY_FAVORITE, hero.getFavorite());
        values.put(KEY_SKILL1_ICON, hero.getSkill1_icon());
        values.put(KEY_SKILL1_DESCRIPTION, hero.getSkill1_description());
        values.put(KEY_SKILL2_ICON, hero.getSkill1_icon());
        values.put(KEY_SKILL2_DESCRIPTION, hero.getSkill2_description());
        values.put(KEY_SKILL3_ICON, hero.getSkill1_icon());
        values.put(KEY_SKILL3_DESCRIPTION, hero.getSkill3_description());
        values.put(KEY_SKILL4_ICON, hero.getSkill1_icon());
        values.put(KEY_SKILL4_DESCRIPTION, hero.getSkill4_description());
        // 3. updating row
        int i = db.update(TABLE_HEROES, //table
                values, // column/value
                KEY_NAME+" = ?", // selections
                new String[] { hero.getName() }); //selection args
        // 4. close
        db.close();

        return i;

    }
    // Deleting single hero
    public void deleteHero(Hero hero) {
        // 1. get reference to writable DB
        SQLiteDatabase db = this.getWritableDatabase();
        // 2. delete
        db.delete(TABLE_HEROES,
                KEY_NAME+" = ?",
                new String[] { hero.getName() });
        // 3. close
        db.close();
    }
}

这里我设置的主键是英雄名,查找起来可能会方便点吧,因此要求英雄不能重名

增加英雄

静态添加:

MainActivity.java中会初始化一些英雄数据,放到数据库里

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //添加数据库
    heroSQLiteHelper = new HeroSQLiteHelper(this);
    heroSQLiteHelper.deleteAllHeroes();
    //下面用来添加数据
    heroSQLiteHelper.addHero(new Hero("橘右京", "android.resource://" + getApplicationContext().getPackageName() + "/" +R.mipmap.juyoujing, "神梦一刀", "刺客" , 5, 6, 5 ,5,
            "android.resource://com.team1.kingofhonor/" + R.raw.juyoujing,
            "android.resource://com.team1.kingofhonor/" + R.mipmap.juyoujing_icon, true,
            "android.resource://com.team1.kingofhonor/" + R.mipmap.juyoujing1, "秘剑胧刀 冷却值:5消耗:0\n" +
            "\n" +
            "被动:橘右京将下一次普攻将进行一次强力拔刀斩,对前方敌人造成130%物理加成物理伤害并减少50%移动速度,持续2秒;拔刀斩每5秒可准备1次;对处于攻击边缘的敌人将承受50%的额外伤害.",
            "android.resource://com.team1.kingofhonor/" + R.mipmap.juyoujing2, "燕返 冷却值:7消耗:0\n" +
            "\n" +
            "橘右京跃向后方,同时向前方挥刀,对附近的敌人造成200/240/280/320/360/400(+145%物理加成)点物理伤害,若成功命中一名敌方英雄,可激活二段使用",
            "android.resource://com.team1.kingofhonor/" + R.mipmap.juyoujing3, "居合 冷却值:10/9.5/9/8.5/8/7.5消耗:0\n" +
            "\n" +
            "橘右京向指定方向快速拔刀,对路径上的第一个敌人造成330/375/420/465/510/555(+212%物理加成)点物理伤害,对路上的其余敌人造成的伤害将衰减50%,并将路径末端的敌人眩晕0.75秒",
            "android.resource://com.team1.kingofhonor/" + R.mipmap.juyoujing4, "细雪 冷却值:12/11.5/11消耗:0\n" +
            "\n" +
            "橘右京向指定方向连续拔刀四次,每次命中造成100/150/200(+70%物理加成)点物理伤害并且每次命中敌方英雄,自身将回复70/100/130(+35%物理加成)点生命值(命中非英雄单位效果减半)"));
    ...
}

Fragment1.java就可以通过数据库中的英雄数据填补Adapter

mySQLiteHelper= new HeroSQLiteHelper(getContext());
mAdapter = new HeroAdapter(mySQLiteHelper.getAllHeroes());

动态添加:

在主界面点击右上角的加号可以进入到一个新的Activity

GIF

activity_hero_add.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:scrollbars="vertical"
    tools:context=".HeroAdd">
    <Toolbar
        android:id="@+id/hero_add_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimaryDark"
        android:navigationIcon="@mipmap/back">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:text="添加英雄"
            android:gravity="center"
            android:textSize="20sp"
            android:textColor="@color/white"/>
    </Toolbar>
    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scrollbars="vertical">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <ImageButton
                android:id="@+id/hero_add_image"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="0dp"
                android:scaleType="fitXY"
                android:adjustViewBounds="true"
                android:src="@mipmap/juyoujing" />

            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:orientation="horizontal">
                <TextView
                    android:id="@+id/hero_add_name_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="英雄名称:"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    android:gravity="center"/>
                <EditText
                    android:id="@+id/hero_add_name"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:hint="橘右京"/>
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:background="@color/colorPrimary"/>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:orientation="horizontal">
                <TextView
                    android:id="@+id/hero_add_alias_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    android:text="英雄称号:" />
                <EditText
                    android:id="@+id/hero_add_alias"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:hint="神梦一刀" />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:background="@color/colorPrimary"/>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:orientation="horizontal">
                <TextView
                    android:id="@+id/hero_add_category_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    android:text="英雄职业:" />
                <Spinner
                    android:id="@+id/hero_add_category"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:entries="@array/herocategory_add"/>
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:background="@color/colorPrimary"/>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:layout_marginTop="5dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="生存能力:"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    />
                <org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
                    android:id="@+id/hero_add_viability"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:dsb_min="1"
                    app:dsb_max="9"
                    app:dsb_progressColor="@color/lightblue"
                    android:layout_marginEnd="10dp"
                    />
            </LinearLayout>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="攻击伤害:"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    />
                <org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
                    android:id="@+id/hero_add_attack_damage"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:dsb_min="1"
                    app:dsb_max="9"
                    app:dsb_progressColor="@color/red"
                    android:layout_marginEnd="10dp"
                    />
            </LinearLayout>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="技能伤害:"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    />
                <org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
                    android:id="@+id/hero_add_skill_damage"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:dsb_min="1"
                    app:dsb_max="9"
                    app:dsb_progressColor="@color/purple"
                    android:layout_marginEnd="10dp"
                    />
            </LinearLayout>
            <LinearLayout
                android:layout_marginStart="10dp"
                android:layout_width="wrap_content"
                android:layout_height="40dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:text="上手难度:"
                    android:textSize="22sp"
                    android:layout_marginEnd="10dp"
                    />
                <org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
                    android:id="@+id/hero_add_difficulty"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    app:dsb_min="1"
                    app:dsb_max="9"
                    app:dsb_progressColor="@color/chocolate"
                    android:layout_marginEnd="10dp"
                    />
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="2dp"
                android:background="@color/colorPrimary"/>
        </LinearLayout>
    </ScrollView>
</LinearLayout>

HeroAdd.java

能添加的属性有英雄名称,英雄称号,英雄职业,英雄头像,英雄海报,英雄语音,英雄生存能力、攻击伤害、技能伤害、上手难度(没有添加技能)

其实大部分属性都可以不填就能添加成功,只有英雄名称一定要填写(因为且不止因为它是数据库表的主键),然后就有一个判断重名的问题,这是的做法是从Fagment1ToolBar跳转过来的时候传入一个保存所有英雄名的数组,添加的时候如果新加的英雄名在这个数组里面就会添加失败!

//比较无重复的英雄名称
hero_names = getIntent().getStringArrayListExtra("hero_names");

当然需要在Adapter新加一个获取所有英雄名称的方法

//返回所有的英雄数据
public ArrayList<String> getAllNames(){
    ArrayList<String> data = new ArrayList<>();
    for(Hero temp : mDatas){
        data.add(temp.getName());
    }
    return data;
}

选取图片

有两个地方需要添加图片,一个直接点击屏幕上的海报图片选取添加英雄的图片,一个是右上角的菜单添加英雄的头像

GIF

以点击页面上的ImageButton(中间那个大图片)为例

通过下面的代码调用了系统的文件选择器,当然把文件类型局限在图片

这里的 startActivityForResult传递的参数是1,是为了跟另外一种选取图片的方式作区别,不然选择的图片不知道该作为英雄的海报还是英雄的头像

imageButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent,1);
    }
});

重写onActivityResult方法,用来选取图片返回结果的处理

这里我做的处理就是把图片复制一份到该应用的file文件夹里(使用BitmapFactory),再保存新图片的Uri,其中新图片的名字由UUID生成,如果是海报的话还会更新页面上的图片按钮

不这么做而是直接保存选取图片的Uri会有两个问题

  • 如果以后删除了相册了这个图片,那么引用就会有问题
  • 更大的问题是其实这次对话结束后,以后再访问这个Uri的图片会出下权限错误,因为这个ACTION_GET_CONTENT权限是一次性的,后来即使知道Uri也不能访问
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == 1){
        if (resultCode == RESULT_OK) {
            Uri uri = data.getData();
            ContentResolver cr = this.getContentResolver();
            try {
                Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri));
                //通过UUID生成字符串文件名
                String image_name = UUID.randomUUID().toString() + ".jpg";
                //存储图片
                FileOutputStream out = openFileOutput(image_name, MODE_PRIVATE);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                //获取复制后文件的uri
                Uri image_file_uri = Uri.fromFile(getFileStreamPath(image_name));
                //图片预览
                imageButton.setImageURI(image_file_uri);
                //保存该URI
                image_uri = image_file_uri.toString();
                out.flush();
                out.close();
            } catch (FileNotFoundException e) {
                Log.e("FileNotFoundException", e.getMessage(),e);
            } catch (IOException e) {
                Log.w("IOException", e.getMessage(), e);
            }
        }
    }else if(requestCode == 2){
        if (resultCode == RESULT_OK) {
            Uri uri = data.getData();
            ContentResolver cr = this.getContentResolver();
            try {
                Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri));
                //通过UUID生成字符串文件名
                String image_name = UUID.randomUUID().toString() + ".jpg";
                //存储图片
                FileOutputStream out = openFileOutput(image_name, MODE_PRIVATE);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                //获取复制后文件的uri
                Uri image_file_uri = Uri.fromFile(getFileStreamPath(image_name));
                //保存该URI
                icon_uri = image_file_uri.toString();
                out.flush();
                out.close();
            } catch (FileNotFoundException e) {
                Log.e("FileNotFoundException", e.getMessage(),e);
            } catch (IOException e) {
                Log.w("IOException", e.getMessage(), e);
            }
        }
        ...

选取语音

GIF

在菜单栏里调用系统文件选取器

这里将ResultCode设置为3,跟前面选取图片作区分

//添加语音
case  R.id.action_hero_add_voice:
    intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.setType("audio/*");
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    startActivityForResult(intent,3);
    break;

对返回的结果做解析,这里同样是复制音频,然后也会预览,预览的办法就是调用MediaPlayer播放这个音频

这也算遇到的一个挺大的问题吧,图片还可以通过BitmapFactory编码解码,但是音频我还不知道怎么编码解码,好像网上很多人都是自己写方法调用,于是想了一个很简单的办法,就是直接把这个文件转换为字节流复制过去(所以这跟前面图片的复制本质上是不一样的)

else  if(requestCode == 3){
    if (resultCode == RESULT_OK) {
        voice_uri = data.getData().toString();
        ContentResolver cr = this.getContentResolver();
        //播放预览
        mediaPlayer = MediaPlayer.create(HeroAdd.this, Uri.parse(voice_uri));
        mediaPlayer.start();
        try {
            //通过UUID生成字符串文件名
            String voice_name = UUID.randomUUID().toString() + ".mp3";
            //存储音频
            FileOutputStream out = openFileOutput(voice_name, MODE_PRIVATE);
            InputStream in = cr.openInputStream(data.getData());
            byte[] newVoice = new byte[in.available()];
            in.read(newVoice);
            out.write(newVoice);
            in.close();
            out.close();
            voice_uri = Uri.fromFile(getFileStreamPath(voice_name)).toString();

        } catch (FileNotFoundException e) {
            Log.e("FileNotFoundException", e.getMessage(),e);
        } catch (IOException e) {
            Log.w("IOException", e.getMessage(), e);
        }
    }

播放音频的后续工作就是记得在Activity销毁的时候记得销毁这个MediaPlayer

@Override
protected void onDestroy() {
    super.onDestroy();
    if(mediaPlayer != null)
        mediaPlayer.release();
}

选择职业

这里使用了一个Spinner控件

GIF
<Spinner
    android:id="@+id/hero_add_category"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:entries="@array/herocategory_add"/>

展开它能选择的值定义在values文件夹中新建一个xml文件(文件名任意取)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    </string-array>
    <string-array name="herocategory_add">
        <item>法师</item>
        <item>刺客</item>
        <item>射手</item>
        <item>辅助</item>
        <item>战士</item>
        <item>坦克</item>
    </string-array>
</resources>

获取选择的值也很简单

final Spinner hero_category = findViewById(R.id.hero_add_category);
//获取值
hero_category.getSelectedItem().toString();

选择生存能力等值

这里使用了一个GitHub的第三方控件org.adw.library.widgets.discreteseekbar.DiscreteSeekBar

原地址

GIF
<org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
    android:id="@+id/hero_add_viability"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:dsb_min="1"
    app:dsb_max="9"
    app:dsb_progressColor="@color/lightblue"
    android:layout_marginEnd="10dp"
    />

HeroAdd.java中获取选中的值

final DiscreteSeekBar hero_viability = findViewById(R.id.hero_add_viability);
hero_viability.getProgress()

保存结果

点击菜单中的保存按钮的事件

这里有三种情况

  • 英雄名为空
  • 英雄名重复(跟传进来的已有英雄数组比较)
  • 添加成功

三种情况都会弹出Toast提示

//设置菜单点击事件
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem menuItem) {
        switch (menuItem.getItemId()) {
            //保存
            case R.id.action_hero_add_save:
                if(hero_name.getText().toString().isEmpty()){
                    Toast.makeText(HeroAdd.this, "英雄名不能为空!", Toast.LENGTH_SHORT).show();
                    return false;
                }
                for(String i : hero_names){
                    if(i.equals(hero_name.getText().toString())){
                        Toast.makeText(HeroAdd.this, "英雄名重复!", Toast.LENGTH_SHORT).show();
                        return false;
                    }

                }
                add_hero = new Hero(hero_name.getText().toString(), image_uri,  hero_alias.getText().toString(),hero_category.getSelectedItem().toString(),
                        hero_viability.getProgress(),hero_attack_damage.getProgress(),hero_skill_damage.getProgress(), hero_difficulty.getProgress(),voice_uri,icon_uri,false);
                add_hero.setAdded(true);
                EventBus.getDefault().post(add_hero);
                finish();
                Toast.makeText(HeroAdd.this, "添加成功", Toast.LENGTH_SHORT).show();
                break;
                ......

但是主要问题是如何把添加的英雄传给主界面呢(因为这里不做英雄数据库的操作),

或者说通知主界面新添加了一个英雄呢(点击ToolBar的返回按钮或者虚拟按键返回会放弃添加英雄)

最简单的方法当然是EventBus

而且还有用到了Hero类的一个属性

private Boolean added = false;//true表示是新加的

向下面这样子把新加的Hero传递回去,并且调用finish方法表示结束该Acitivity

这样子只要在Fragment1.java中的EventBus接收方法中判断英雄的added属性是否为true就可以判断是否要把这个英雄加到数据库里了

之所以要设置added这个变量是为了跟后面的deleted,modified分别表示的已删除,已修改作区分

根本宗旨就是数据库的更新全部放在英雄主界面(Fragment1)其它页面只能传递参数告诉它要如何更改数据库

add_hero.setAdded(true);
EventBus.getDefault().post(add_hero);
finish();

更新数据库

如上面讨论的这个得在Fragment1.java中实现

添加EventBus的处理方法

判断传递过来的Heroadded属性是否为true,是的话就添加到Adapter

还要添加到SQLiteHelper中持久存储

//EventBus
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(Hero h) {
    if(h.getAdded()){
        h.setAdded(false);
        searchAdapter.add(h.getName());
        //searchAdapter.notifyDataSetChanged();
        mAdapter.addNewItem(h);
        mySQLiteHelper.addHero(h);
        //更新当前选中的英雄类型的列表
        mAdapter.updateWithCategory(mySQLiteHelper.getAllHeroes(), ((RadioButton)view.findViewById(hero_category_select.getCheckedRadioButtonId())).getText().toString());
        Log.d("添加英雄", "onMessageEvent: add" + h.getName());
    } else if
        .......

};

//记得注册
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        EventBus.getDefault().register(this);
    ......
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容