前两节我们分别分析了网易评论列表界面和生成一些我们需要的测试数据,生成测试数据那段如果大家看着看得头疼没关系,直接调业务对象中的方法生成数据即可不必理会我是怎么处理的,接下来的对于大家来说才是让各位感兴趣的东西

界面分析了、数据也有了,那我们如何实现这样的一个界面呢?首先我们来看一下整个项目的结构图大致了解下:

MainActivity是该应用的入口Activity,里面就对ActionBar和Fragment做了一些初始化:

package com.aigestudio.neteasecommentlistdemo.activities;import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;import com.aigestudio.neteasecommentlistdemo.R;
import com.aigestudio.neteasecommentlistdemo.bo.SQLiteDataBO;
import com.aigestudio.neteasecommentlistdemo.fragment.CommentFragment;/*** 应用的入口Activity* 没有做太多的逻辑,除了ActionBar所有的界面元素都集成在Fragment中** @author Aige* @since 2014/11/14*/
public class MainActivity extends ActionBarActivity {private ActionBar actionBar;//状态栏private SQLiteDataBO sqLiteDataBO;//数据业务对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//初始化控件initWidget();//初始化数据:一次即可,如果你clean了项目需要重新生成数据,生成数据前前注释掉上面的initWidget()初始化控件方法
//        sqLiteDataBO = new SQLiteDataBO(this);
//        sqLiteDataBO.initServerData();}/*** 初始化控件*/private void initWidget() {//初始化ActionBarinitActionBar();//设置当前显示的FragmentgetSupportFragmentManager().beginTransaction().add(R.id.container, new CommentFragment()).commit();}/*** 初始化ActionBar*/private void initActionBar() {actionBar = getSupportActionBar();actionBar.setDisplayShowTitleEnabled(false);actionBar.setDisplayShowHomeEnabled(true);actionBar.setHomeButtonEnabled(true);actionBar.setDisplayHomeAsUpEnabled(true);}
}

重点在CommentFragment类里面,在该类里面我们获取数据库的数据并将其传入Adapter

package com.aigestudio.neteasecommentlistdemo.fragment;import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;import com.aigestudio.neteasecommentlistdemo.R;
import com.aigestudio.neteasecommentlistdemo.beans.Post;
import com.aigestudio.neteasecommentlistdemo.bo.CommentFMBO;
import com.aigestudio.neteasecommentlistdemo.dao.ServerDAO;
import com.aigestudio.neteasecommentlistdemo.views.PostView;import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;/*** 唯一的一个Fragment用来显示界面** @author Aige* @since 2014/11/14*/
public class CommentFragment extends Fragment {private ListView lvContent;//填充内容的List列表private ServerDAO serverDAO;//服务器数据的访问对象private CommentFMBO commentFMBO;//业务对象private List<Post> posts;//存储帖子的列表@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//初始化服务器数据库DAOserverDAO = new ServerDAO(getActivity());//初始化存储帖子的列表posts = new ArrayList<Post>();//初始化业务对象commentFMBO = new CommentFMBO(serverDAO);}@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {//获取根布局View rootView = inflater.inflate(R.layout.fragment_comment, container, false);//获取ListView控件lvContent = (ListView) rootView.findViewById(R.id.comment_fm_content_lv);//初始化数据initData();return rootView;}/*** 初始化数据* <p/>* 注:数据的加载方式非按实际方式以远端服务器异步加载,So……别钻空子~~*/private void <span style="color:#990000;"><strong>initData</strong></span>() {//查询赞前十的帖子List<Map<String, String>> praiseTop10Post = serverDAO.queryMulti("user_praise", new String[]{"postFlag"}, null, null, "postFlag", null, "count(postFlag) desc", "10");
//        List<Map<String, String>> praiseTop10Post = serverDAO.queryMulti("select postFlag from user_praise group by postFlag order by count(postFlag) desc limit 10");//查询Post数据posts = commentFMBO.queryPost(praiseTop10Post, "postFlag", posts, Post.Type.HOTTEST);//查询最新的十条帖子数据List<Map<String, String>> newestTop10Posts = serverDAO.queryMulti("post", new String[]{"flag"}, null, null, null, null, "_id desc", "10");
//        List<Map<String, String>> newestTop10Posts = serverDAO.queryMulti("select flag from post order by count(_id) desc limit 10");//查询Post数据posts = commentFMBO.queryPost(newestTop10Posts, "flag", posts, Post.Type.NEWEST);//数据验证
//        commentFMBO.verifyData(posts);//数据加载lvContent.setAdapter(new CommentAdapter(posts));}private class CommentAdapter extends BaseAdapter {private List<Post> posts;private CommentAdapter(List<Post> posts) {this.posts = posts;}@Overridepublic int getCount() {return posts.size();}@Overridepublic Object getItem(int position) {return null;}@Overridepublic long getItemId(int position) {return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {if (null==convertView){convertView = new PostView(getActivity());}((PostView)convertView).setPost(posts.get(position));return convertView;}}
}

该类的代码并不多,主要的无非initData()方法中数据的获取封装,其内部逻辑我们也封装在了对应的业务对象CommentFMBO中,CommentFMBO对外公布的方法也就两个:一个用来查询Post数据的List<Post> queryPost(List<Map<String, String>> postList, String key, List<Post> posts, Post.Type type)方法和一个用来验证数据的verifyData(List<Post> posts)方法,其中验证数据的方法是我们在生成数据后对数据正确性的一个测试,所以真正对我们有用的方法就一个queryPost

/*** 查询Post数据** @param postList Post数据源*/
public List<Post> queryPost(List<Map<String, String>> postList, String key, List<Post> posts, Post.Type type) {for (int i = 0; i < postList.size(); i++) {//实例化一个Post对象Post post = new Post();/*判断帖子的类型是否为最新的或最热的,如果是则将第一条帖子的Type设置为相应类型*/if (type != Post.Type.NORMAL && i == 0) {post.setType(type);} else {post.setType(Post.Type.NORMAL);}//设置该Post的标识值post.setFlag(postList.get(i).get(key));//设置该Post的创建时间String createAt = serverDAO.queryValue("post", new String[]{"createAt"}, "flag", post.getFlag());
//                String createAt = serverDAO.queryValue("select createAt from post where flag like " + post.getFlag());post.setCreateAt(createAt);//设置该Post的评论列表List<Comment> comments = getComments(postList, i, key);post.setComments(comments);//设置该Post赞的User列表List<User> praises = getUserPraises(postList, i, key);post.setUserPraises(praises);//设置该Post踩的User列表List<User> unPraises = getUserUnPraises(postList, i, key);post.setUserUnPraises(unPraises);//设置该Post收藏的User列表List<User> collects = getUserCollects(postList, i, key);post.setUserCollects(collects);posts.add(post);}return posts;
}

CommentFMBO类中其他的一些实现有兴趣大家可以看我公布的源码这里就不多说了。在拿到Post列表后我们就将其通过CommentAdapter的构造函数注入CommentAdapter,而在CommentAdapter的getView中我们的逻辑也非常简单

@Override
public View getView(int position, View convertView, ViewGroup parent) {if (null==convertView){convertView = new PostView(getActivity());}((PostView)convertView).setPost(posts.get(position));return convertView;
}

这段代码相信大家一看就懂,convertView为空时我们才去new一个自定义的PostView控件否则直接调PostView控件中的setPost方法去设置数据 ,不知道大家在看这段代码的时候有没有这样一个疑问,为什么不通过PostView的构造函数注入数据呢?为什么要单独给出一个方法来设置数据? 大家可以自己思考下。下面我们来看看自定义控件PostView中有些什么东西:

package com.aigestudio.neteasecommentlistdemo.views;import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;import com.aigestudio.neteasecommentlistdemo.R;
import com.aigestudio.neteasecommentlistdemo.beans.Comment;
import com.aigestudio.neteasecommentlistdemo.beans.Post;import java.util.List;/*** 用来显示Post的自定义控件** @author Aige* @since 2014/11/14*/
public class PostView extends LinearLayout {private TextView tvType, tvUserName, tvLocation, tvDate, tvPraise, tvContent;//依次为显示类型标签、用户名、地理位置、日期、赞数据和最后一条评论内容的TextViewprivate CircleImageView civNick;//用户圆形头像显示控件private FloorView floorView;//盖楼控件public PostView(Context context) {this(context, null);}public PostView(Context context, AttributeSet attrs) {this(context, attrs, 0);}@SuppressLint("NewApi")public PostView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//初始化控件initWidget(context);}/*** 初始化控件** @param context 上下文环境引用*/private void initWidget(Context context) {//设置布局LayoutInflater.from(context).inflate(R.layout.view_post, this);//获取控件tvType = (TextView) findViewById(R.id.view_post_type_tv);tvUserName = (TextView) findViewById(R.id.view_post_username_tv);tvLocation = (TextView) findViewById(R.id.view_post_location_tv);tvDate = (TextView) findViewById(R.id.view_post_date_tv);tvPraise = (TextView) findViewById(R.id.view_post_praise_tv);tvContent = (TextView) findViewById(R.id.view_post_content_tv);civNick = (CircleImageView) findViewById(R.id.view_post_nick_civ);floorView = (FloorView) findViewById(R.id.view_post_floor_fv);}/*** 为PostView设置数据** @param post 数据源*/public void setPost(Post post) {//设置Post的类型setType(post);//设置Post的赞数据setPraise(post);//获取该条帖子下的评论列表List<Comment> comments = post.getComments();/*判断评论长度1.如果只有一条评论那么则显示该评论即可并隐藏盖楼布局2.否则我们进行盖楼显示*/if (comments.size() == 1) {floorView.setVisibility(GONE);Comment comment = comments.get(0);//设置控件显示数据initUserDate(comment);} else {//盖楼前我们要把最后一条评论数据提出来显示在Post最外层int index = comments.size() - 1;Comment comment = comments.get(index);//设置控件显示数据initUserDate(comment);floorView.setComments(comments);}}/*** 设置与用户相关的控件数据显示** @param comment 评论对象*/private void initUserDate(Comment comment) {tvContent.setText(comment.getContent());tvDate.setText(comment.getCreateAt());tvUserName.setText(comment.getUser().getUserName());tvLocation.setText(comment.getUser().getLocation());civNick.setImageResource(Integer.parseInt(comment.getUser().getNick()));}/*** 设置Post的赞数据** @param post 数据源*/private void setPraise(Post post) {tvPraise.setText(post.getUserPraises().size() + "赞");}/*** 设置Post的类型** @param post 数据源*/private void setType(Post post) {//获取Post类型Post.Type type = post.getType();/*设置类型显示*/switch (type) {case NEWEST:tvType.setVisibility(VISIBLE);tvType.setText("最新跟帖");break;case HOTTEST:tvType.setVisibility(VISIBLE);tvType.setText("热门跟帖");break;case NORMAL:tvType.setVisibility(GONE);break;}}
}

PostView是一个继承于LinearLayout的复合控件,里面我们设置了一个布局,布局的xml我就不贴出来了,可以给大家看下该布局的效果如下:

在PostView被实例化的时候我们就在initWidget(Context context)方法中初始化其布局,而设置其PostView显示数据的方法我们独立在setPost(Post post)方法中,说白了就是数据和显示的分离,为什么要这样做?很简单,即便我当前的PostView被重用了,我也可以通过setPost(Post post)方法重新设置我们的数据而不需要重新再实例化一个PostView也不用担心PostView在Item中顺序混淆,更不用担心多次地去findView造成的效率问题,因为findView的过程只在实例化的时候才会去做,设置数据不需要再管

在setPost(Post post)方法中除了获取封装的数据并设置PostView上各控件的显示数据外我们还要进行Comment的判断:

/*
判断评论长度
1.如果只有一条评论那么则显示该评论即可并隐藏盖楼布局
2.否则我们进行盖楼显示*/
if (comments.size() == 1) {floorView.setVisibility(GONE);Comment comment = comments.get(0);//设置控件显示数据initUserDate(comment);
} else {//盖楼前我们要把最后一条评论数据提出来显示在Post最外层int index = comments.size() - 1;Comment comment = comments.get(index);//设置控件显示数据initUserDate(comment);floorView.setComments(comments);
}

如代码所示,当评论只有一条时我们就不显示盖楼了,如果评论大于一条那么我们就要显示盖楼的FloorView,但是我们会先把评论中最后一条数据提取出来显示在PostView上。盖楼的控件FloorView也是一个复合控件,盖楼的原理很简单,数据我们自上而下按时间顺序(我按的_id,懒得去计算时间了~~)依次显示,FloorView绘制子View前我们先把整个盖楼层叠效果的背景画出来,然后再让FloorView去绘制子View:

package com.aigestudio.neteasecommentlistdemo.views;import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;import com.aigestudio.neteasecommentlistdemo.R;
import com.aigestudio.neteasecommentlistdemo.beans.Comment;
import com.aigestudio.neteasecommentlistdemo.beans.User;import java.util.List;/*** 用来显示PostView中盖楼的自定义控件** @author Aige* @since 2014/11/14*/
public class FloorView extends LinearLayout {private Context context;//上下文环境引用private Drawable drawable;//背景Drawablepublic FloorView(Context context) {this(context, null);}public FloorView(Context context, AttributeSet attrs) {this(context, attrs, 0);}@SuppressLint("NewApi")public FloorView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);this.context = context;//获取背景Drawable的资源文件drawable = context.getResources().getDrawable(R.drawable.view_post_comment_bg);}/*** 设置Comment数据** @param comments Comment数据列表*/public void setComments(List<Comment> comments) {//清除子ViewremoveAllViews();//获取评论数int count = comments.size();/*如果评论条数小于9条则直接显示,否则我们只显示评论的头两条和最后一条(这里的最后一条是相对于PostView中已经显示的一条评论来说的)*/if (count < 9) {initViewWithAll(comments);} else {initViewWithHide(comments);}}/*** 初始化所有的View** @param comments 评论数据列表*/private void initViewWithAll(List<Comment> comments) {for (int i = 0; i < comments.size() - 1; i++) {View commentView = getView(comments.get(i), i, comments.size() - 1, false);addView(commentView);}}/*** 初始化带有隐藏楼层的View** @param comments 评论数据列表*/private void initViewWithHide(final List<Comment> comments) {View commentView = null;//初始化一楼commentView = getView(comments.get(0), 0, comments.size() - 1, false);addView(commentView);//初始化二楼commentView = getView(comments.get(1), 1, comments.size() - 1, false);addView(commentView);//初始化隐藏楼层标识commentView = getView(null, 2, comments.size() - 1, true);commentView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {initViewWithAll(comments);}});addView(commentView);//初始化倒数第二楼commentView = getView(comments.get(comments.size() - 2), 3, comments.size() - 1, false);addView(commentView);}/*** 获取单个评论子视图** @param comment 评论对象* @param index   第几个评论* @param count   总共有几个评论* @param isHide  是否是隐藏显示* @return 一个评论子视图*/private View getView(Comment comment, int index, int count, boolean isHide) {//获取根布局View commentView = LayoutInflater.from(context).inflate(R.layout.view_post_comment, null);//获取控件TextView tvUserName = (TextView) commentView.findViewById(R.id.view_post_comment_username_tv);TextView tvContent = (TextView) commentView.findViewById(R.id.view_post_comment_content_tv);TextView tvNum = (TextView) commentView.findViewById(R.id.view_post_comment_num_tv);TextView tvHide = (TextView) commentView.findViewById(R.id.view_post_comment_hide_tv);/*判断是否是隐藏楼层*/if (isHide) {/*是则显示“点击显示隐藏楼层”控件而隐藏其他的不相干控件*/tvUserName.setVisibility(GONE);tvContent.setVisibility(GONE);tvNum.setVisibility(GONE);tvHide.setVisibility(VISIBLE);} else {/*否则隐藏“点击显示隐藏楼层”控件而显示其他的不相干控件*/tvUserName.setVisibility(VISIBLE);tvContent.setVisibility(VISIBLE);tvNum.setVisibility(VISIBLE);tvHide.setVisibility(GONE);//获取用户对象User user = comment.getUser();//设置显示数据tvUserName.setText(user.getUserName());tvContent.setText(comment.getContent());tvNum.setText(String.valueOf(index + 1));}//设置布局参数LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);//计算margin指数,这个指数的意义在于将第一个的margin值设置为最大的,然后依次递减体现层叠效果int marginIndex = count - index;int margin = marginIndex * 3;params.setMargins(margin, 0, margin, 0);commentView.setLayoutParams(params);return commentView;}@Overrideprotected void dispatchDraw(Canvas canvas) {/*在FloorView绘制子控件前先绘制层叠的背景图片*/for (int i = getChildCount() - 1; i >= 0; i--) {View view = getChildAt(i);drawable.setBounds(view.getLeft(), view.getLeft(), view.getRight(), view.getBottom());drawable.draw(canvas);}super.dispatchDraw(canvas);}
}

是不是很简单呢? 我相信稍有基础的童鞋都能看懂,没有复杂的逻辑没有繁杂的计算过程,唯一的一个计算就是margin的计算,其原理也如代码注释所说的那样并不难,背景图片的绘制使用了我们事先定义的一个drawable资源:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="#222225"/><strokeandroid:width="1px"android:color="#777775"/>
</shape>

我依稀记得有个很逗的孩纸问我评论上的边框是怎么画的………… 。这里要提醒大家一点就是在dispatchDraw方法中记得要先画背景再去调用父类的super.dispatchDraw(canvas);方法去画子View,不然就会出现背景把所有控件都遮挡的效果。这个背景是FloorView的,而不是单个子评论控件的,也就是说我们其实是做了个假象,绘制了这么一张图:

昨天有个童鞋问这样的框框是如何画的……首先我纠正,这一个框是一张背景Drawable而不是一个“框”,这张背景Drawable的外观样式是我们在xml文件中预先定义好加载的:

//获取背景Drawable的资源文件
drawable = context.getResources().getDrawable(R.drawable.view_post_comment_bg);

在一个ViewGroup(比如我们的FloorView extends LinearLayout)中我们通过dispatchDraw(Canvas canvas)方法来绘制子控件,在这之前ViewGroup会分别调用measureXXX和layout方法来分别测量和确定绘制自身和子控件的位置,具体的实现大家可以在网上找到一大堆相关的文章在这就不多说了。而在dispatchDraw方法中我们可以获取到每个子View的大小和位置信息,因为我们的FloorView是一个线性布局,并且我们在xml中设置其排列方式为垂直排列的,每当我们往其中添加一个view这个view就会排列在上一个view下方:

在父容器measure和layout之后~~所有的子控件大小位置将被确定,我们可以得到子View相对于父控件的left、top、right和bottom:


回到我们的代码:

@Override
protected void dispatchDraw(Canvas canvas) {/*在FloorView绘制子控件前先绘制层叠的背景图片*/for (int i = getChildCount() - 1; i >= 0; i--) {View view = getChildAt(i);drawable.setBounds(view.getLeft(), view.getLeft(), view.getRight(), view.getBottom());drawable.draw(canvas);}super.dispatchDraw(canvas);
}

我们首先获取了最下方的那个子View,而在上面的margin计算中最下面的子View我们的margin=0;如果不考虑父控件的padding的话此时这个位于最下方的子View的getLeft()=0,也就是说其距离父控件左边的距离为0,而倒数第二个子View的margin应该为3,getLeft()=3,倒数第三个子View的margin=6,getLeft()=6………………以此类推直至最上方的子View,我们在绘制背景的时候也是按照这样的顺序:最底层的drawable起始坐标为x=getLeft(),y=getLeft(),也就是(0,0),宽和高分别为view.getRight()和view.getBottom(),依次计算下去直至最后一个View~~~~~UnderStand

这就是整个评论列表的实现过程,源码在此:传送门,IDE为Studio,如果你用Eclipse,做一个代码的搬运工即可~

对于大家来说可能码代码这一过程是最重要的,其实对于我来说,前期对实现的分析才是最重要的,如果分析得不对实现的过程就会巨繁琐,举个栗子如果我们在分析界面的时候得出每个Item中元素的关系为“评论—>回复”结构,那么不但我们的数据设计要繁琐,界面的展示也难以得到我们想要的效果~~~~还是那句话,牛逼的体现不在于用复杂的技术实现复杂的效果,而是用简单的方法得到复杂的效果~~~~

实现过程就是这样,但是依然有一些不足,在这我留给大家两个问题去思考下:

1.我们都知道findView是一个很耗时的过程,因为我们要从xml文档中解析出各个节点,解析xml文档是很废时的,也正基于此,在我们自定义BaseAdapter的时候我们会在getView方法中通过一个ViewHolder对象存储已经find的控件并复用他以此来提高效率。而在我们的FloorView的getView方法中我们会不断地去从xml文档中解析控件:

private View getView(Comment comment, int index, int count, boolean isHide) {//获取根布局View commentView = LayoutInflater.from(context).inflate(R.layout.view_post_comment, null);//获取控件TextView tvUserName = (TextView) commentView.findViewById(R.id.view_post_comment_username_tv);TextView tvContent = (TextView) commentView.findViewById(R.id.view_post_comment_content_tv);TextView tvNum = (TextView) commentView.findViewById(R.id.view_post_comment_num_tv);TextView tvHide = (TextView) commentView.findViewById(R.id.view_post_comment_hide_tv);/*………………………………………………………………………………………………………………………………………………………………*/return commentView;
}

这个过程是非常恶心的,我们是否也可以用一个对象来存储它并实现复用呢?

2.在dispatchDraw中绘制背景图的时候,我们会根据所有子View的location来绘制drawable,这个过程是again and again并且一层一层地画……事实上有必要吗?

这两问题就交给大家解决了,下一篇我将会给大家讲讲如何优化这个界面使之更高效!

高仿网易评论列表效果之界面生成相关推荐

  1. 高仿网易评论列表效果之界面分析

    Hello大家好我是周杰伦~!@#¥#@¥%¥%--%&*&--**)--*%&¥%#¥!!!! 不好意思,刚忘了吃药了~~~~扯正事,前几天有个小哥来面试,因为前些天面试了很 ...

  2. Node.js+MySQL开发的B2C商城系统源码+数据库(微信小程序端+服务端),界面高仿网易严选商城

    下载地址:Node.js+MySQL开发的B2C商城系统源码+数据库(微信小程序端+服务端) NideShop商城(微信小程序端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易 ...

  3. 基于android的高仿抖音,Android仿抖音列表效果

    本文实例为大家分享了Android仿抖音列表效果的具体代码,供大家参考,具体内容如下 当下抖音非常火热,是不是也很心动做一个类似的app吗? 那我们就用RecyclerView实现这个功能吧,关于内存 ...

  4. Android高仿抖音滚动聊天,Android仿抖音列表效果

    本文实例为大家分享了Android仿抖音列表效果的具体代码,供大家参考,具体内容如下 当下抖音非常火热,是不是也很心动做一个类似的app吗? 那我们就用RecyclerView实现这个功能吧,关于内存 ...

  5. 小程序 node.js mysql_基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选)...

    高仿网易严选的微信小程序商城(微信小程序客户端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+Think ...

  6. node 小程序 php,基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选)...

    高仿网易严选的微信小程序商城(微信小程序客户端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+Think ...

  7. 基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选)

    高仿网易严选的微信小程序商城(微信小程序客户端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+Think ...

  8. 基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选) 1

    高仿网易严选的微信小程序商城(微信小程序客户端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+Think ...

  9. ionic2入门教程(三)高仿网易公开课(1)

    Ionic2系列之高仿网易公开课(1) 0.登录界面实现截图和官方图片对比 我的 官方 1.新建一个blank项目 打开cmd,输入ionic start Ionic-NetEaseOpenCours ...

  10. Qt:一个简洁漂亮的高仿网易云播放器

    Qt:一个简洁漂亮的高仿网易云播放器 界面动图展示: 功能简述: 1.音乐的播放.暂停 2.音乐的上.下一曲 3.进度条显示进度.滑动条调整进度 4.音量的滑动调整 5.列表切换歌曲 6.播放.暂停的 ...

最新文章

  1. python导入csv文件-python如何导入csv文件格式
  2. java feature get_Java ShapeFeature.getLocations方法代码示例
  3. 找出数组中任一重复的数字
  4. 网易智慧企业亮相TOP 100 Summit,以创新和匠心探索行业前沿
  5. 使用jstree创建无限分级的树(ajax动态创建子节点)
  6. 茌平计算机中考成绩查询,中考成绩查询系统入口2021
  7. Android之Fragment
  8. 互联网产品总监的经验总结:从0-1为你讲明白BI与数据可视化
  9. TurboMail邮件系统提醒广大用户小心DXXD勒索邮件
  10. cruzer php sandisk 闪迪u盘量产工具_sandisk量产工具(闪迪U盘量产工具) 1.4
  11. Linux扩容raid,linux raid1扩容的方法
  12. Python爬取NBA球员生涯数据及简单可视化
  13. 【春节闲聊】程序员如何打破35岁魔咒
  14. 该内存不能written
  15. 如何修改mind map pro 的快捷键 how to edit shortcut of mind map pro
  16. 百度天气预报API的使用(java版本)
  17. Xshell6 提示要继续使用此程序,您必须应用最新的更新或使用新版本
  18. linux脚本命令同时起多个命令行窗口
  19. linux下的go富集分析,GO富集分析(转载)-Go语言中文社区
  20. Java小程序,编写一个迷你DVD租借系统(控制台输出)

热门文章

  1. java sub_java调用zeromq PUB-SUB模式
  2. 计算机网络电子邮件的基本格式,怎样的格式才是正确的电子邮件格式?
  3. 什么情况下会用上568A线序
  4. 转载:h5标签中的embed标签
  5. Android OTA升级
  6. 台式计算机的cpu,台式电脑处理器(CPU)性能排行榜
  7. 统计分析软件_强大的多元统计分析软件-Mplus
  8. 2022最新整理新手零基础系统的自学网络安全
  9. 20172328《程序设计与数据结构》第二周学习总结
  10. 如何做专利挖掘,关键是寻找专利点,其实并不太难