高仿知乎日报

已完成功能

  • 启动动画与启动图片的获取
  • 文章类型的展示与缓存
  • 文章列表的展示与缓存
  • 文章内容的展示
  • 今日热闻界面的轮播控件
  • 下拉刷新
  • 文章浏览到底部时自动加载更多
  • 标记已读文章
  • 自动缓存文章
  • 夜间模式

依赖的开源项目

Gson
Android-Universal-Image-Loader
android-async-http

依赖的官方支持包

  • com.android.support:support-v4:22.2.1
  • com.android.support:appcompat-v7:22.2.1
  • com.android.support:design:22.2.1

亮点

  • Android Design Support Library的使用
    • Snackbar
    • CoordinatorLayout
    • AppBarLayout
    • CollapsingToolbarLayout
  • Activity切换的Reveal效果

效果:

项目地址:http://download.csdn.net/detail/linder_qzy/9533280
编译时如果报错org.apache.http.Header这个类找不
则在build.gradle中
添加apache http component 的依赖,补全缺失的类,比如Header:
dependencies {
    compile 'org.apache.httpcomponents:httpcore:4.4.2'
}


高仿知乎日报(一)

之前写的高仿知乎日报的代码是用eclipse写的,导入AndroidStudio之后虽然改改也能跑起来,但是格式很怪异,而且由于时间长了,很多东西都忘了,所以准备用AndroidStudio重写一遍,顺便记录下过程并将很多需要优化的地方都完成。 
首先是接口的获取,不需要自己去抓包分析,因为已经有人分析过了: 
知乎日报接口  
既然接口都有了,那就装上知乎日报app照着搞呗。 
1.首先编写Application,由于使用到了UIL框架,所以在Application中初始化它。
package krelve.app.kuaihu;
import android.app.Application;
import android.content.Context;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import com.nostra13.universalimageloader.utils.StorageUtils;
import java.io.File;

/**
 * Created by wwjun.wang on 2015/8/11.
 */
public class Kpplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        initImageLoader(getApplicationContext());
    }

    private void initImageLoader(Context context) {
        File cacheDir = StorageUtils.getCacheDirectory(context);
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(
                context).threadPoolSize(3)
                .threadPriority(Thread.NORM_PRIORITY - 2)
                .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
                .denyCacheImageMultipleSizesInMemory()
                .diskCacheFileNameGenerator(new Md5FileNameGenerator())
                .tasksProcessingOrder(QueueProcessingType.LIFO)
                .diskCache(new UnlimitedDiskCache(cacheDir)).writeDebugLogs()
                .build();
        ImageLoader.getInstance().init(config);

    }
} 

2.打开知乎日报,发现会有一个启动页,包含一个放大图片的动画,实现起来很简单:  
简单的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center_vertical">

    <ImageView
        android:id="@+id/iv_start"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:adjustViewBounds="true"
        android:scaleType="fitXY" />
</RelativeLayout>

界面代码
package krelve.app.kuaihu.activity;
import android.app.Activity;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.widget.ImageView;
import com.loopj.android.http.AsyncHttpResponseHandler;
import com.loopj.android.http.BinaryHttpResponseHandler;
import org.apache.http.Header;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import krelve.app.kuaihu.R;
import krelve.app.kuaihu.util.Constant;
import krelve.app.kuaihu.util.HttpUtils;

/**
 * Created by wwjun.wang on 2015/8/11.
 */
public class SplashActivity extends Activity {
    private ImageView iv_start;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.splash);
        iv_start = (ImageView) findViewById(R.id.iv_start);
        initImage();
    }

    private void initImage() {
        File dir = getFilesDir();
        final File imgFile = new File(dir, "start.jpg");
        if (imgFile.exists()) {
            iv_start.setImageBitmap(BitmapFactory.decodeFile(imgFile.getAbsolutePath()));
        } else {
            iv_start.setImageResource(R.mipmap.start);
        }
        final ScaleAnimation scaleAnim = new ScaleAnimation(1.0f, 1.2f, 1.0f, 1.2f,
                Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        scaleAnim.setFillAfter(true);
        scaleAnim.setDuration(3000);
        scaleAnim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                HttpUtils.get(Constant.START, new AsyncHttpResponseHandler() {
                    @Override
                    public void onSuccess(int i, Header[] headers, byte[] bytes) {
                        try {
                            JSONObject jsonObject = new JSONObject(new String(bytes));
                            String url = jsonObject.getString("img");
                            HttpUtils.get(url, new BinaryHttpResponseHandler() {
                                @Override
                                public void onSuccess(int i, Header[] headers, byte[] bytes) {
                                    saveImage(imgFile, bytes);
                                    startActivity();
                                }

                                @Override
                                public void onFailure(int i, Header[] headers, byte[] bytes, Throwable throwable) {
                                    startActivity();
                                }
                            });

                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onFailure(int i, Header[] headers, byte[] bytes, Throwable throwable) {
                        startActivity();
                    }
                });
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        iv_start.startAnimation(scaleAnim);

    }

    private void startActivity() {
        Intent intent = new Intent(SplashActivity.this, MainActivity.class);
        startActivity(intent);
        overridePendingTransition(android.R.anim.fade_in,
                android.R.anim.fade_out);
        finish();
    }

    public void saveImage(File file, byte[] bytes) {
        try {
            file.delete();
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(bytes);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
} 

在知乎日报的接口中,我们会看到这样一个接口: 
http://news-at.zhihu.com/api/4/start-image/1080*1776 
用来获取启动界面的图像,所以在启动时,要去获取最新的启动图像。 
这里用到了android-http-async框架和UIM框架,在Module的build.gradle文件中添加:

 compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.4' compile 'com.loopj.android:android-async-http:1.4.8'

还要记得在manifest文件中加权限:

 <uses-permission android:name="android.permission.INTERNET"></uses-permission> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission> <uses-permission android:name="android.permission.write_external_storage"></uses-permission>

这样基本就完成了一个启动页面,很简单。 
3.主界面布局: 
先来一个效果图: 

可以看到,用到了Toolbar和DrawerLayout还有SwipeRefreshLayout。 
布局文件

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawerlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/sr"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="?attr/colorPrimaryDark"
                android:theme="@style/MyActionBar" />


            <FrameLayout
                android:id="@+id/fl_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"></FrameLayout>
        </LinearLayout>
    </android.support.v4.widget.SwipeRefreshLayout>

    <fragment
        android:name="krelve.app.kuaihu.fragment.MenuFragment"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_gravity="left" />

</android.support.v4.widget.DrawerLayout>
可以看到Toolbar的位置在DrawerLayout的里面,如果想要侧滑的时候侧滑菜单显示在Toolbar的下面,只需让Toolbar的位置在DrawerLayout外面就行。 
这里需要注意的是Toolbar的样式,如果仔细看了知乎日报的Toolbar就会发现它用的应该是Dark类型的主题,因为这个Toolbar除了背景其它地方都是白色的,那好,我们直接给Toolbar设置 android:theme=ThemeOverlay.AppCompat.ActionBar  
然后问题就来了,再点开右侧overflow,会发现弹出的菜单背景是黑色的。 
这可不行,清新的风格瞬间被毁,于是在style.xml中定义自己的style:

 <style name="MyActionBar" parent="ThemeOverlay.AppCompat.ActionBar"> <!--<item name="android:actionOverflowButtonStyle">@style/MyOverflowButton</item>--> <item name="android:textColor">@android:color/black</item> </style> 
给我们的Toolbar引用这个style就达到了目的。 
那就剩下侧滑菜单的编写了,本来是想用NavigationView来实现的,但是效果不怎么理想,还是自己写吧。 
布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="360dp"
    android:layout_height="match_parent"
    android:clickable="true"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_dark"
        android:orientation="vertical"
        android:paddingBottom="10dp">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginTop="15dp"
            android:orientation="horizontal">

            <ImageView
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:background="@drawable/ic_account_circle_white_24dp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="36dp"
                android:layout_marginLeft="10dp"
                android:gravity="center_vertical"
                android:text="请登录"
                android:textColor="@android:color/white"
                android:textSize="18sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="25dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:layout_weight="1"
                android:drawableLeft="@drawable/ic_star_white_24dp"
                android:gravity="center"
                android:text="我的收藏"
                android:textColor="@android:color/white"
                android:textSize="15sp" />

            <TextView
                android:id="@+id/tv_download"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginLeft="20dp"
                android:layout_weight="1"
                android:drawableLeft="@drawable/ic_file_download_white_24dp"
                android:gravity="center"
                android:text="离线下载"
                android:textColor="@android:color/white"
                android:textSize="15sp" />
        </LinearLayout>
    </LinearLayout>

    <TextView
        android:id="@+id/tv_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#FFF0F0F0"
        android:paddingBottom="10dp"
        android:paddingLeft="80dp"
        android:paddingTop="10dp"
        android:text="首页"
        android:textColor="@android:color/holo_blue_dark"
        android:textSize="18sp" />

    <ListView
        android:id="@+id/lv_item"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:divider="@android:color/transparent"
        android:scrollbars="none"></ListView>
</LinearLayout>
关键点是ListView,这里要显示的数据我们要通过接口 http://news-at.zhihu.com/api/4/themes 来获取,为了简化网络操作,对android-http-async进行了及其简单的封装:
package krelve.app.kuaihu.util;

import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.ResponseHandlerInterface;

/**
 * Created by wwjun.wang on 2015/8/11.
 */
public class HttpUtils {
    private static AsyncHttpClient client = new AsyncHttpClient();

    public static void get(String url, ResponseHandlerInterface responseHandler) {
        client.get(Constant.BASEURL + url, responseHandler);
    }

} 

获取到的都是Json格式的字串,由于现在遇到的json格式比较简单,所以直接用android中自带的json解析库来解析,在后面会用到Gson直接向bean中映射。 
贴上整个Fragment的代码:
package krelve.app.kuaihu.fragment;

import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

import com.loopj.android.http.JsonHttpResponseHandler;

import org.apache.http.Header;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

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

import krelve.app.kuaihu.R;
import krelve.app.kuaihu.model.NewsListItem;
import krelve.app.kuaihu.util.Constant;
import krelve.app.kuaihu.util.HttpUtils;

public class MenuFragment extends Fragment implements OnClickListener {
    private ListView lv_item;
    private TextView tv_download, tv_main; // private static String[] ITEMS = { "日常心理学", "用户推荐日报", "电影日报", "不许无聊", // "设计日报", "大公司日报", "财经日报", "互联网安全", "开始游戏", "音乐日报", "动漫日报", "体育日报" }; private List<NewsListItem> items; private Handler handler = new Handler(); @Override public View onCreateView(LayoutInflater inflater,
    @Nullable
    ViewGroup container,
    @Nullable
    Bundle savedInstanceState
    )

    {
        View view = inflater.inflate(R.layout.menu, container, false);
        tv_download = (TextView) view.findViewById(R.id.tv_download);
        tv_download.setOnClickListener(this);
        tv_main = (TextView) view.findViewById(R.id.tv_main);
        tv_main.setOnClickListener(this);
        lv_item = (ListView) view.findViewById(R.id.lv_item);
        getItems();
        return view;
    }

    private void getItems() {
        items = new ArrayList<NewsListItem>();
        HttpUtils.get(Constant.THEMES, new JsonHttpResponseHandler() {
            @Override
            public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
                super.onSuccess(statusCode, headers, response);
                try {
                    JSONArray itemsArray = response.getJSONArray("others");
                    for (int i = 0; i < itemsArray.length(); i++) {
                        NewsListItem newsListItem = new NewsListItem();
                        JSONObject itemObject = itemsArray.getJSONObject(i);
                        newsListItem.setTitle(itemObject.getString("name"));
                        newsListItem.setId(itemObject.getString("id"));
                        items.add(newsListItem);
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            lv_item.setAdapter(new NewsTypeAdapter());
                        }
                    });
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });


    }

    public class NewsTypeAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            return items.size();
        }

        @Override
        public Object getItem(int position) {
            return items.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (convertView == null) {
                convertView = LayoutInflater.from(getActivity()).inflate(
                        R.layout.menu_item, parent, false);
            }
            TextView tv_item = (TextView) convertView
                    .findViewById(R.id.tv_item);
            tv_item.setText(items.get(position).getTitle());
            return convertView;
        }
    }

    @Override
    public void onClick(View v) {
    }
}
} 

NewsListItem.java:
package krelve.app.kuaihu.model;

public class NewsListItem {
    private String title;
    private String id;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

} 

menu_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_item"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:paddingBottom="10dp"
    android:paddingLeft="15dp"
    android:paddingTop="10dp"
    android:text="新闻条目"
    android:textColor="#FF000000"
    android:textSize="16dp" /> 
这样大概就完成了整个主界面的编写,如果有疏漏的地方,可以到github上看完整代码,我会根据进度实时上传。

上一篇文章中完成了一个大致的界面,这次呢主要是完成主页面的展示。先做出一个最新消息的展示效果。 
展示一下: 
 
这就是要显示的效果,最上面是一个图片轮播控件,下面则是文章列表。 
那好,让我们从头分析一下:根据滚动效果,可以看出这个整体是一个ListView,图片轮播控件则被当作它的headerView,下面则是各个文章。还需要注意的是今日热闻,需要在定义ListView的adapter时注意一下。 
先贴上图片轮播控件的代码,这个效果很常见,所以我把这个控件抽取出来放到了github上,方便使用:Kanner 
当然,这个控件最关键的是提供一种通用情况,像现在这种需要在轮播的图片上显示文字的情况,则需要再额外添加。 
Kanner.java:
package krelve.app.kuaihu.view;

import android.content.Context;
import android.os.Handler;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.nostra13.universalimageloader.core.ImageLoader;

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

import krelve.app.kuaihu.Kpplication;
import krelve.app.kuaihu.R;
import krelve.app.kuaihu.model.Latest;

public class Kanner extends FrameLayout implements OnClickListener {
    private List<Latest.TopStoriesEntity> topStoriesEntities;
    private ImageLoader mImageLoader;
    private List<View> views;
    private Context context;
    private ViewPager vp;
    private boolean isAutoPlay;
    private int currentItem;
    private int delayTime;
    private LinearLayout ll_dot;
    private List<ImageView> iv_dots;
    private Handler handler = new Handler();
    private OnItemClickListener mItemClickListener;

    public Kanner(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mImageLoader = ImageLoader.getInstance();
        this.context = context;
        initView();
    }

    private void initView() {
        views = new ArrayList<View>();
        iv_dots = new ArrayList<ImageView>();
        delayTime = 2000;
    }

    public Kanner(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Kanner(Context context) {
        this(context, null);
    }

    public void setTopEntities(List<Latest.TopStoriesEntity> topEntities) {
        this.topStoriesEntities = topEntities;
        reset();
    }

    private void reset() {
        views.clear();
        initUI();
    }

    private void initUI() {
        View view = LayoutInflater.from(context).inflate(
                R.layout.kanner_layout, this, true);
        vp = (ViewPager) view.findViewById(R.id.vp);
        ll_dot = (LinearLayout) view.findViewById(R.id.ll_dot);
        ll_dot.removeAllViews();
        int len = topStoriesEntities.size();
        for (int i = 0; i < len; i++) {
            ImageView iv_dot = new ImageView(context);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            params.leftMargin = 5;
            params.rightMargin = 5;
            ll_dot.addView(iv_dot, params);
            iv_dots.add(iv_dot);
        }
        for (int i = 0; i <= len + 1; i++) {
            View fm = LayoutInflater.from(context).inflate(
                    R.layout.kanner_content_layout, null);
            ImageView iv = (ImageView) fm.findViewById(R.id.iv_title);
            TextView tv_title = (TextView) fm.findViewById(R.id.tv_title);
            iv.setScaleType(ScaleType.CENTER_CROP);
            iv.setBackgroundResource(R.drawable.loading1);
            if (i == 0) {
                mImageLoader.displayImage(topStoriesEntities.get(len - 1).getImage(), iv);
                tv_title.setText(topStoriesEntities.get(len - 1).getTitle());
            } else if (i == len + 1) {
                mImageLoader.displayImage(topStoriesEntities.get(0).getImage(), iv);
                tv_title.setText(topStoriesEntities.get(0).getTitle());
            } else {
                mImageLoader.displayImage(topStoriesEntities.get(i - 1).getImage(), iv);
                tv_title.setText(topStoriesEntities.get(i - 1).getTitle());
            }
            fm.setOnClickListener(this);
            views.add(fm);
        }
        vp.setAdapter(new MyPagerAdapter());
        vp.setFocusable(true);
        vp.setCurrentItem(1);
        currentItem = 1;
        vp.addOnPageChangeListener(new MyOnPageChangeListener());
        startPlay();
    }

    private void startPlay() {
        isAutoPlay = true;
        handler.postDelayed(task, 3000);
    }

    private final Runnable task = new Runnable() {
        @Override
        public void run() {
            if (isAutoPlay) {
                currentItem = currentItem % (topStoriesEntities.size() + 1) + 1;
                if (currentItem == 1) {
                    vp.setCurrentItem(currentItem, false);
                    handler.post(task);
                } else {
                    vp.setCurrentItem(currentItem);
                    handler.postDelayed(task, 5000);
                }
            } else {
                handler.postDelayed(task, 5000);
            }
        }
    };

    class MyPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return views.size();
        }

        @Override
        public boolean isViewFromObject(View arg0, Object arg1) {
            return arg0 == arg1;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            container.addView(views.get(position));
            return views.get(position);
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

    }

    class MyOnPageChangeListener implements OnPageChangeListener {
        @Override
        public void onPageScrollStateChanged(int arg0) {
            switch (arg0) {
                case 1:
                    isAutoPlay = false;
                    break;
                case 2:
                    isAutoPlay = true;
                    break;
                case 0:
                    if (vp.getCurrentItem() == 0) {
                        vp.setCurrentItem(topStoriesEntities.size(), false);
                    } else if (vp.getCurrentItem() == topStoriesEntities.size() + 1) {
                        vp.setCurrentItem(1, false);
                    }
                    currentItem = vp.getCurrentItem();
                    isAutoPlay = true;
                    break;
            }
        }

        @Override
        public void onPageScrolled(int arg0, float arg1, int arg2) {
        }

        @Override
        public void onPageSelected(int arg0) {
            for (int i = 0; i < iv_dots.size(); i++) {
                if (i == arg0 - 1) {
                    iv_dots.get(i).setImageResource(R.drawable.dot_focus);
                } else {
                    iv_dots.get(i).setImageResource(R.drawable.dot_blur);
                }
            }

        }

    }

    public void setOnItemClickListener(OnItemClickListener mItemClickListener) {
        this.mItemClickListener = mItemClickListener;
    }

    public interface OnItemClickListener {
        public void click(Latest.TopStoriesEntity entity);
    }

    @Override
    public void onClick(View v) {
        if (mItemClickListener != null) {
            Latest.TopStoriesEntity entity = topStoriesEntities.get(vp.getCurrentItem() - 1);
            mItemClickListener.click(entity);
        }
    }
} 
还有布局文件kanner_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:id="@+id/ll_dot"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:gravity="right"
        android:orientation="horizontal"
        android:padding="8dp"></LinearLayout>
</merge>
以及要显示的内容的布局文件kanner_content_layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_title"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="25dp"
        android:padding="10dp"
        android:text="标题"
        android:textColor="@android:color/white"
        android:textSize="20sp" />
</FrameLayout>
那接下来就是如何设置这个控件要显示的数据了: 
MainFragment.java:
package krelve.app.kuaihu.fragment;

import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ListView;

import com.google.gson.Gson;
import com.loopj.android.http.TextHttpResponseHandler;

import org.apache.http.Header;

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

import krelve.app.kuaihu.R;
import krelve.app.kuaihu.activity.MainActivity;
import krelve.app.kuaihu.adapter.NewsItemAdapter;
import krelve.app.kuaihu.model.Latest;
import krelve.app.kuaihu.util.Constant;
import krelve.app.kuaihu.util.HttpUtils;
import krelve.app.kuaihu.view.Kanner;

/**
 * Created by wwjun.wang on 2015/8/12.
 */
public class MainFragment extends BaseFragment {
    private ListView lv_news;
    private List<Latest> items;
    private Latest latest;
    private Kanner kanner;
    private Handler handler = new Handler();

    @Override
    protected View initView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.main_news_layout, container, false);
        lv_news = (ListView) view.findViewById(R.id.lv_news);
        View header = inflater.inflate(R.layout.kanner, lv_news, false);
        kanner = (Kanner) header.findViewById(R.id.kanner);
        kanner.setOnItemClickListener(new Kanner.OnItemClickListener() {
            @Override
            public void click(Latest.TopStoriesEntity entity) {

            }
        });
        lv_news.addHeaderView(header);
        lv_news.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {

            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (lv_news != null && lv_news.getChildCount() > 0) {
                    boolean enable = (firstVisibleItem == 0) && (view.getChildAt(firstVisibleItem).getTop() == 0);
                    ((MainActivity) mActivity).setSwipeRefreshEnable(enable);
                }
            }
        });
        return view;
    }

    @Override
    protected void initData() {
        super.initData();
        HttpUtils.get(Constant.LATESTNEWS, new TextHttpResponseHandler() {
            @Override
            public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
            }

            @Override
            public void onSuccess(int statusCode, Header[] headers, String responseString) {
                Gson gson = new Gson();
                latest = gson.fromJson(responseString, Latest.class);
                kanner.setTopEntities(latest.getTop_stories());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        List<Latest.StoriesEntity> storiesEntities = latest.getStories();
                        Latest.StoriesEntity topic = new Latest.StoriesEntity();
                        topic.setType(Constant.TOPIC);
                        topic.setTitle("今日热闻");
                        storiesEntities.add(0, topic);
                        lv_news.setAdapter(new NewsItemAdapter(mActivity, storiesEntities));
                    }
                });
            }

        });
    }
} 
可以看到,在解析数据的时候,用到了Gson这个开源库,直接将json格式的数据映射到实体bean中。 
Latest.java:
package krelve.app.kuaihu.model;

import java.util.List;
/**
 * Created by wwjun.wang on 2015/8/12.
 */
public class Latest {
    /**
     * top_stories : [{"id":7048089,"title":"发生类似天津爆炸事故时,该如何自救?","ga_prefix":"081309","image":"http://pic4.zhimg.com/494dafbd64c141fd023d4e58b3343fcb.jpg","type":0},{"id":7047383,"title":"每卖一辆车亏 4000 美元,这事儿跟「iPhone 成本仅几百元」挺像","ga_prefix":"081307","image":"http://pic1.zhimg.com/40e0f21292df0e8512385f191e71ad14.jpg","type":0},{"id":7047795,"title":"央视说要干预男男性行为,具体是怎么干预法?","ga_prefix":"081310","image":"http://pic4.zhimg.com/89f0bca7d4ccf70bd747f3675adc18eb.jpg","type":0},{"id":7047071,"title":"美国人最爱买的车第一名是它,第二名是它,第三名,还是它\u2026\u2026","ga_prefix":"081307","image":"http://pic1.zhimg.com/9c00c482251e82fa8e0b957fa9ceb334.jpg","type":0},{"id":7046751,"title":"今晚的修破斯哒是 · 小李子","ga_prefix":"081219","image":"http://pic3.zhimg.com/bc5f63634d9c9832da8593ac64ebb7d6.jpg","type":0}]
     * stories : [{"id":7047795,"title":"央视说要干预男男性行为,具体是怎么干预法?","ga_prefix":"081310","images":["http://pic3.zhimg.com/fe27abc8f094510f2d3b4f3706108b56.jpg"],"type":0},{"id":7048089,"title":"发生类似天津爆炸事故时,该如何自救?","ga_prefix":"081309","images":["http://pic1.zhimg.com/eabb48a57948dc405429d0c0185c7950.jpg"],"type":0},{"id":7047188,"title":"分析了一下,发现这几个中国城市不光物价高,而且收入低","ga_prefix":"081308","images":["http://pic2.zhimg.com/7ce0dfe918b11069bc857421876e6609.jpg"],"type":0},{"id":7047383,"title":"每卖一辆车亏 4000 美元,这事儿跟「iPhone 成本仅几百元」挺像","ga_prefix":"081307","images":["http://pic2.zhimg.com/b8319323c8f8e3ec0ccc80d4745305a9.jpg"],"type":0},{"id":7047071,"title":"美国人最爱买的车第一名是它,第二名是它,第三名,还是它\u2026\u2026","ga_prefix":"081307","images":["http://pic2.zhimg.com/822b9ce452e8d48e6d32b83d4c1ea6e9.jpg"],"type":0},{"id":7047484,"title":"理论上就业率对工资很重要,但是在中国没这么回事","ga_prefix":"081307","images":["http://pic4.zhimg.com/d08952ed50050efdcee33203a4225ba3.jpg"],"type":0},{"id":7047181,"title":"瞎扯 · 如何正确地吐槽","ga_prefix":"081306","images":["http://pic4.zhimg.com/1dd6304067619318034671af9cf26803.jpg"],"type":0}]
     * date : 20150813
     */
    private List<TopStoriesEntity> top_stories;
    private List<StoriesEntity> stories;
    private String date;
    public void setTop_stories(List<TopStoriesEntity> top_stories) {
        this.top_stories = top_stories;
    }
    public void setStories(List<StoriesEntity> stories) {
        this.stories = stories;
    }

    public void setDate(String date) {
        this.date = date;
    }
    public List<TopStoriesEntity> getTop_stories() {
        return top_stories;
    }
    public List<StoriesEntity> getStories() {
        return stories;
    }
    public String getDate() {
        return date;
    }

    public static class TopStoriesEntity {
        /**
         * id : 7048089
         * title : 发生类似天津爆炸事故时,该如何自救?
         * ga_prefix : 081309
         * image : http://pic4.zhimg.com/494dafbd64c141fd023d4e58b3343fcb.jpg
         * type : 0
         */
        private int id;
        private String title;
        private String ga_prefix;
        private String image;
        private int type;

        public void setId(int id) {
            this.id = id;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public void setGa_prefix(String ga_prefix) {
            this.ga_prefix = ga_prefix;
        }

        public void setImage(String image) {
            this.image = image;
        }

        public void setType(int type) {
            this.type = type;
        }

        public int getId() {
            return id;
        }

        public String getTitle() {
            return title;
        }

        public String getGa_prefix() {
            return ga_prefix;
        }

        public String getImage() {
            return image;
        }

        public int getType() {
            return type;
        }
        @Override
        public String toString() {
            return "TopStoriesEntity{" + "id=" + id + ", title='" + title + '\'' + ", ga_prefix='" + ga_prefix + '\'' + ", image='" + image + '\'' + ", type=" + type + '}';
        }
    }

    public static class StoriesEntity {
        /**
         * id : 7047795
         * title : 央视说要干预男男性行为,具体是怎么干预法?
         * ga_prefix : 081310
         * images : ["http://pic3.zhimg.com/fe27abc8f094510f2d3b4f3706108b56.jpg"]
         * type : 0
         */
        private int id;
        private String title;
        private String ga_prefix;
        private List<String> images;
        private int type;
        public void setId(int id) {
            this.id = id;
        }
        public void setTitle(String title) {
            this.title = title;
        }

        public void setGa_prefix(String ga_prefix) {
            this.ga_prefix = ga_prefix;
        }
        public void setImages(List<String> images) {
            this.images = images;
        }
        public void setType(int type) {
            this.type = type;
        }
        public int getId() {
            return id;
        }
        public String getTitle() {
            return title;
        }
        public String getGa_prefix() {
            return ga_prefix;
        }
        public List<String> getImages() {
            return images;
        }
        public int getType() {
            return type;
        }
        @Override
        public String toString() {
            return "StoriesEntity{" + "id=" + id + ", title='" + title + '\'' + ", ga_prefix='" + ga_prefix + '\'' + ", images=" + images + ", type=" + type + '}';
        }
    }
    @Override
    public String toString() {
        return "Latest{" + "top_stories=" + top_stories + ", stories=" + stories + ", date='" + date + '\'' + '}';
    }
} 
看到这个实体bean的时候是不是感觉很复杂,写的时候很不好写? 
不要担心,我们有 GsonFormat 这个插件,将需要解析的json数据输入,便可以自动生成对应的实体bean,简直不能更爽。 
还有一个关键点是冲突的解决,由于使用了SwipeRefreshListener这个下拉刷新控件,而ListView又包含在其中,那再下拉的时候如果不处理,必然会起冲突。 
其实解决起来很简单,我们只需要让下拉刷新的操作在ListView被滑动到最顶部的时候进行就可以了:
lv_news.setOnScrollListener(new AbsListView.OnScrollListener(){
@Override public void onScrollStateChanged(AbsListView view,int scrollState){

        }
@Override public void onScroll(AbsListView view,int firstVisibleItem,int visibleItemCount,int totalItemCount){if(lv_news!=null&&lv_news.getChildCount()>0){boolean enable=(firstVisibleItem==0)&&(view.getChildAt(firstVisibleItem).getTop()==0);
        ((MainActivity)mActivity).setSwipeRefreshEnable(enable);
        }
        }
        });
现在大概就实现了今日热闻的展示,那接下来还要实现下拉刷新和自动加载更多,以及其他内容页的展示,今天就写这么多,有关ListView中每一条内容的展示布局等可以在源码中看,最新内容已同步到github


高仿知乎日报(三)

在上一篇文章中完成了 今日热闻 的相关代码,这次来完成侧滑菜单中的点击响应,每个item都有对应的页面。 
先上效果图: 
 
在写各类文章的界面前,先把 今日热闻 的下拉刷新逻辑和自动加载更多的功能完成。 
还记得之前处理swiperefreshlayout与listview的滑动冲突吗?就是在那里添加:
 @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (lv_news != null && lv_news.getChildCount() > 0) { boolean enable = (firstVisibleItem == 0) && (view.getChildAt(firstVisibleItem).getTop() == 0);
                    ((MainActivity) mActivity).setSwipeRefreshEnable(enable); if (firstVisibleItem + visibleItemCount == totalItemCount && !isLoading) {
                        loadMore(Constant.BEFORE + date);
                    }
                }

            }
loadMore方法:
private void loadMore(final String url) {
        isLoading = true;
        HttpUtils.get(url, new TextHttpResponseHandler() { @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
            } @Override public void onSuccess(int statusCode, Header[] headers, String responseString) {
                Gson gson = new Gson();
                before = gson.fromJson(responseString, Before.class);
                date = before.getDate();
                handler.post(new Runnable() { @Override public void run() {
                        List<StoriesEntity> storiesEntities = before.getStories();
                        StoriesEntity topic = new StoriesEntity();
                        topic.setType(Constant.TOPIC);
                        topic.setTitle(convertDate(date));
                        storiesEntities.add(0, topic);
                        mAdapter.addList(storiesEntities);
                        isLoading = false;
                    }
                });
            }

        });
    }

还有自动刷新的逻辑实现(目前只实现了今日热闻的刷新):

 sr.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() {
                replaceFragment();
                sr.setRefreshing(false);
            }
        }); public void replaceFragment() { if (curId.equals("latest")) {
            getSupportFragmentManager().beginTransaction().setCustomAnimations(R.anim.slide_in_from_right, R.anim.slide_out_to_left)
                    .replace(R.id.fl_content, new MainFragment(), "latest").commit();
        } else {

        }

    }

然后就是今天的重点了,首先去实现侧滑菜单栏的点击事件:

 lv_item.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                getFragmentManager()
                        .beginTransaction().setCustomAnimations(R.anim.slide_in_from_right, R.anim.slide_out_to_left)
                        .replace(
                                R.id.fl_content, new NewsFragment(items.get(position)
                                        .getId()), "news").commit();
                ((MainActivity) mActivity).setCurId(items.get(position).getId());
                ((MainActivity) mActivity).closeMenu();
            }
        });

其实就是按照点击的position来获取对应的id,然后发出请求,动态的用Fragment来展示。 
这才是主角:

package krelve.app.kuaihu.fragment;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.google.gson.Gson;
import com.loopj.android.http.TextHttpResponseHandler;
import com.nostra13.universalimageloader.core.ImageLoader;

import org.apache.http.Header;

import java.util.ArrayList;

import krelve.app.kuaihu.R;
import krelve.app.kuaihu.activity.MainActivity;
import krelve.app.kuaihu.adapter.NewsItemAdapter;
import krelve.app.kuaihu.model.News;
import krelve.app.kuaihu.util.Constant;
import krelve.app.kuaihu.util.HttpUtils;

/**
 * Created by wwjun.wang on 2015/8/14.
 */
@SuppressLint("ValidFragment")
public class NewsFragment extends BaseFragment {
    private ImageLoader mImageLoader;
    private ListView lv_news;
    private ImageView iv_title;
    private TextView tv_title;
    private String urlId;
    private News news;

    public NewsFragment(String id) {
        urlId = id;
    }

    @Override
    protected View initView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_layout, container, false);
        mImageLoader = ImageLoader.getInstance();
        lv_news = (ListView) view.findViewById(R.id.lv_news);
        View header = LayoutInflater.from(mActivity).inflate(
                R.layout.news_header, lv_news, false);
        iv_title = (ImageView) header.findViewById(R.id.iv_title);
        tv_title = (TextView) header.findViewById(R.id.tv_title);
        lv_news.addHeaderView(header); //        lv_news.setOnItemClickListener(new AdapterView.OnItemClickListener() { // //            @Override //            public void onItemClick(AdapterView<?> parent, View view, //                                    int position, long id) { //                NewsItem newsItem = (NewsItem) parent.getAdapter().getItem( //                        position); //                Intent intent = new Intent(getActivity(), //                        ThemeNewsContentActivity.class); //                intent.putExtra("id", newsItem.getId()); //                intent.putExtra("title", newsItem.getTitle()); //                startActivity(intent); //            } //        }); lv_news.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (lv_news != null && lv_news.getChildCount() > 0) {
            boolean enable = (firstVisibleItem == 0) && (view.getChildAt(firstVisibleItem).getTop() == 0);
            ((MainActivity) mActivity).setSwipeRefreshEnable(enable);
        }
    }
});return view;
        }@Override protected void initData(){super.initData();
        HttpUtils.get(Constant.THEMENEWS+urlId,new TextHttpResponseHandler(){@Override public void onFailure(int statusCode,Header[]headers,String responseString,Throwable throwable){

        }@Override public void onSuccess(int statusCode,Header[]headers,String responseString){
        Gson gson=new Gson();
        news=gson.fromJson(responseString,News.class);
        tv_title.setText(news.getDescription());
        mImageLoader.displayImage(news.getImage(),iv_title);
        lv_news.setAdapter(new NewsItemAdapter(mActivity,news.getStories()));
        }
        });
        }
        } 
在完成了 今日热闻 后,对上面的代码肯定不陌生,因为它们有着惊人的相似,上面用到的所有bean都是用GsonFormat自动生成的,不过由于有很多重复的地方,所以我把 StoriesEntity 抽取了出来。 
看到这里,就会发现其实今天的内容就是对上篇文章的扩展,从个体扩展到通用,所以很多xml文件就不贴了,可以在github上看,今天的内容已经同步上去。 


高仿知乎日报(四)

之前已经大致上把文章的列表都展示了出来,这次就实现点击事件,可以阅读具体的文章内容。 
先上成果: 
 
看了图就知道了,今天的任务是文章内容的展示,和通过 CoordinatorLayout 等一系列design包中的控件来实现滚动效果,以及activity之间跳转的一个全屏扩散的特效,挺不错吧。 
1.文章内容的展示 ,重点就是WebVIew的使用,其实比一般的WebView用的还简单,因为返回的数据中有css文件,直接引入就行,那就看看怎么在WebView中加载css文件吧:

 mWebView = (WebView) findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        HttpUtils.get(Constant.CONTENT + entity.getId(), new TextHttpResponseHandler() { @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {

            } @Override public void onSuccess(int statusCode, Header[] headers, String responseString) {
                Gson gson = new Gson();
                content = gson.fromJson(responseString, Content.class); final ImageLoader imageloader = ImageLoader.getInstance();
                imageloader.displayImage(content.getImage(), iv);
                String css = "<link rel=\"stylesheet\" href=\"file:///android_asset/css/news.css\" type=\"text/css\">";
                String html = "<html><head>" + css + "</head><body>" + content.getBody() + "</body></html>";
                html = html.replace("<div class=\"img-place-holder\">", "");
                mWebView.loadDataWithBaseURL("x-data://base", html, "text/html", "UTF-8", null);
            }
        });
很简单,就是在html代码的头部引入css文件就行,没有必要每次都去下载css文件,而且格式是固定的,所以就直接放到了assets目录中。记住要用loadDataWithBaseURL来加载,直接用loadData会有乱码等一系列问题(返回的html代码是经过转义的)。 
2.滚动效果的实现  
没什么好说的,都是固定的,值得注意的地方就是 CoordinatorLayout 的滚动效果只能与RecyclerView和与 NestedScrollView 有关的一些控件搭配,所以我们给 WebView 外面嵌套一个 NestedScrollView

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

    <krelve.app.kuaihu.view.RevealBackgroundView
        android:id="@+id/revealBackgroundView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/coordinatorLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="230dp"
            android:fitsSystemWindows="true"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsing_toolbar_layout"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:collapsedTitleTextAppearance="@style/MyToolbarTextStyle"
                app:contentScrim="?attr/colorPrimaryDark"
                app:expandedTitleMarginStart="5dp"
                app:expandedTitleTextAppearance="@style/MyToolbarTextStyle"
                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                <ImageView
                    android:id="@+id/iv"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"
                    app:layout_collapseMode="parallax"
                    app:layout_collapseParallaxMultiplier="0.7" />

                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize"
                    app:layout_collapseMode="pin" />
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="vertical"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <WebView
                android:id="@+id/webview"
                android:layout_width="match_parent"
                android:layout_height="match_parent"></WebView>
        </android.support.v4.widget.NestedScrollView>
    </android.support.design.widget.CoordinatorLayout>
</RelativeLayout>
3.activity跳转特效  
在上面的布局文件中可以看到 RevealBackgroundView 这么一个奇怪的东西,其实这就是用来实现点击处全屏扩散效果的自定义View。原理上简单的说就是通过 overridePendingTransition(0, 0) 将activity之间的跳转效果取消,这样一点击就直接显示了新的activity,这时我们将新的activity的背景设置为透明,在界面上播放 RevealBackgroundView 的全屏扩散动画,在播放完后,将所有控件显示出来,就造成了一种界面是从点击处逐步扩散出来的假象。 
代码不多贴了,感兴趣的看源码吧~ 


高仿知乎日报(五)

现在已经实现了高仿知乎日报的基本阅读功能,如果只是想阅读的话,这些已经够了,但既然是高仿,那就尽量多做几个功能吧。 
依然是先上图: 
 
今天主要是做 夜间模式 功能,其实就是个换肤功能,由于只有白天和夜间两种模式,所以做起来也不需要网上流传的那些皮肤方案,直接更新一下背景色和字体颜色就行。 
先将所有用到的颜色抽取到 colors.xml 文件中:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="gray">#BEBEBE</color>

    <color name="light_toolbar">@android:color/holo_blue_dark</color>

    <color name="dark_toolbar">@android:color/black</color>

    <color name="light_news_item">#FFF0F0F0</color>

    <color name="dark_news_item">#DD000000</color>

    <color name="light_news_topic">#ff666666</color>

    <color name="dark_news_topic">#CCFFFFFF</color>

    <color name="dark_selector_drawable">#FF333333</color>

    <color name="light_menu_header">@android:color/holo_blue_dark</color>

    <color name="dark_menu_header">@android:color/black</color>

    <color name="light_menu_header_tv">@android:color/white</color>

    <color name="dark_menu_header_tv">#CCFFFFFF</color>

    <color name="light_menu_index_background">#FFF0F0F0</color>

    <color name="dark_menu_index_background">#FF111111</color>

    <color name="light_menu_listview_background">@android:color/white</color>

    <color name="dark_menu_listview_background">#FF222222</color>

    <color name="light_menu_listview_textcolor">#FF000000</color>

    <color name="dark_menu_listview_textcolor">@android:color/darker_gray</color>
</resources>
设置menu的点击事件:

@Override 
public boolean onOptionsItemSelected(MenuItem item){
        int id=item.getItemId();
        if(id==R.id.action_mode){
                isLight=!isLight;
                toolbar.setBackgroundColor(getResources().getColor(isLight?R.color.light_toolbar:R.color.dark_toolbar));
        if(curId.equals("latest")){
                ((MainFragment)getSupportFragmentManager().findFragmentByTag("latest")).updateTheme();
                }else{
                ((NewsFragment)getSupportFragmentManager().findFragmentByTag("news")).updateTheme();
                }
                ((MenuFragment)getSupportFragmentManager().findFragmentById(R.id.menu_fragment)).updateTheme();
                        sp.edit().putBoolean("isLight",isLight).commit();
                }
                return super.onOptionsItemSelected(item);
        }

可以看到,这里每一个需要更换样式的界面都提供updateTheme这样一个方法,就拿MainFragment来举例:

 public void updateTheme() {
        mAdapter.updateTheme();
    }

在内部调用了adapter的updateTheme方法。

 public void updateTheme() {
        isLight = ((MainActivity) context).isLight();
        notifyDataSetChanged();
    }

而adapter中的updateTheme方法只有两句话,那关键的地方在哪儿呢?既然看到了notifyDataSetChanged,那更换样式肯定实在getView中进行喽。

@Override public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder = null; if (convertView == null) {
            viewHolder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.main_news_item, parent, false);
            viewHolder.tv_topic = (TextView) convertView.findViewById(R.id.tv_topic);
            viewHolder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
            viewHolder.iv_title = (ImageView) convertView.findViewById(R.id.iv_title);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        ((LinearLayout) viewHolder.iv_title.getParent().getParent().getParent()).setBackgroundColor(context.getResources().getColor(isLight ? R.color.light_news_item : R.color.dark_news_item));
        viewHolder.tv_topic.setTextColor(context.getResources().getColor(isLight ? R.color.light_news_topic : R.color.dark_news_topic));
        viewHolder.tv_title.setTextColor(context.getResources().getColor(isLight ? android.R.color.black : android.R.color.white));
        StoriesEntity entity = entities.get(position); if (entity.getType() == Constant.TOPIC) {
            ((FrameLayout) viewHolder.tv_topic.getParent()).setBackgroundColor(Color.TRANSPARENT);
            viewHolder.tv_title.setVisibility(View.GONE);
            viewHolder.iv_title.setVisibility(View.GONE);
            viewHolder.tv_topic.setVisibility(View.VISIBLE);
            viewHolder.tv_topic.setText(entity.getTitle());
        } else {
            ((FrameLayout) viewHolder.tv_topic.getParent()).setBackgroundResource(isLight ? R.drawable.item_background_selector_light : R.drawable.item_background_selector_dark);
            viewHolder.tv_topic.setVisibility(View.GONE);
            viewHolder.tv_title.setVisibility(View.VISIBLE);
            viewHolder.iv_title.setVisibility(View.VISIBLE);
            viewHolder.tv_title.setText(entity.getTitle());
            mImageloader.displayImage(entity.getImages().get(0), viewHolder.iv_title);
        } return convertView;
    }

可以看到,其实是从MainActivity获得isLight的值,然后在代码中判断是否是夜间模式,来动态改变背景色与文字颜色。 
原理就是这样,其它的就不赘述了。


高仿知乎日报(六)

经过前面的几天,大部分功能已经完成了,今天再来添加一个实用的功能,就是离线缓存。 
老规矩,先上图: 
 
在没加缓存前,如果在没联网的情况下打开应用,就连侧滑的菜单都不会显示效果,界面上的文章列表和阅读文章就更不会显示了。 
那让我们一点一点的来添加缓存。 
1.侧滑菜单的缓存 
首先思考一下,什么情况下要更新缓存?考虑到文章的实时性,决定在每次联网的时候都获取最新内容列表,放到缓存中,如果打开应用时判断没有网络,则从缓存中读取。

 if (HttpUtils.isNetworkConnected(mActivity)) {
            HttpUtils.get(Constant.THEMES, new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { super.onSuccess(statusCode, headers, response);
                    String json = response.toString();
                    PreUtils.putStringToDefault(mActivity, Constant.THEMES, json);
                    parseJson(response);
                }
            });
        } else {
            String json = PreUtils.getStringFromDefault(mActivity, Constant.THEMES, ""); try {
                JSONObject jsonObject = new JSONObject(json);
                parseJson(jsonObject);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

再贴出判断网络状态的代码:

 public static boolean isNetworkConnected(Context context) { if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); if (mNetworkInfo != null) { return mNetworkInfo.isAvailable();
            }
        } return false;
    }

看到了吧,其实很简单,就是将url作为keyjson字串作为value存入SharedPreferences,因为获取新闻类型的请求只有一条,所以没有必要用数据库来存,在后面缓存文章列表和文章内容的时候会用到数据库。 
2.文章列表的缓存 
文章列表的缓存就要考虑的多一些了,因为涉及到了图片。不过有了UniversalImageloader,就不用我们多做操心了,直接设置options

 options = new DisplayImageOptions.Builder()
                .cacheInMemory(true)
                .cacheOnDisk(true)
                .build();

在所有需要缓存图片的地方,调用displayImage方法的时候都加上参数options。 
由于今日热闻界面的数据加载涉及到第一次加载和更多加载,所以拿第一次加载来举例,完整代码请看源代码:

 if (HttpUtils.isNetworkConnected(mActivity)) {
            HttpUtils.get(Constant.LATESTNEWS, new TextHttpResponseHandler() { @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {
                } @Override public void onSuccess(int statusCode, Header[] headers, String responseString) {
                    SQLiteDatabase db = ((MainActivity) mActivity).getCacheDbHelper().getWritableDatabase();
                    db.execSQL("replace into CacheList(date,json) values(" + Constant.LATEST_COLUMN + ",' " + responseString + "')");
                    db.close();
                    parseLatestJson(responseString);
                }

            });
        } else {
            SQLiteDatabase db = ((MainActivity) mActivity).getCacheDbHelper().getReadableDatabase();
            Cursor cursor = db.rawQuery("select * from CacheList where date = " + Constant.LATEST_COLUMN, null); if (cursor.moveToFirst()) {
                String json = cursor.getString(cursor.getColumnIndex("json"));
                parseLatestJson(json);
            } else {
                isLoading = false;
            }
            cursor.close();
            db.close();
        }

这里用到了数据库,建了一个很简单的表:

package krelve.app.kuaihu.db;

import android.content.Context;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * Created by wwjun.wang on 2015/8/19.
 */
public class CacheDbHelper extends SQLiteOpenHelper {
    public CacheDbHelper(Context context, int version) {
        super(context, "cache.db", null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("create table if not exists CacheList (id INTEGER primary key autoincrement,date INTEGER unique,json text)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
} 
使用 replace into 是为了防止重复插入,就是如果不存在则插入,存在即更新。 
3.文章内容的缓存  
本来以为文章内容的缓存是最费劲的,没想到WebView自带的缓存功能这么强大…

 mWebView = (WebView) findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); // 开启DOM storage API 功能 mWebView.getSettings().setDomStorageEnabled(true); // 开启database storage API功能 mWebView.getSettings().setDatabaseEnabled(true); // 开启Application Cache功能 mWebView.getSettings().setAppCacheEnabled(true); if (HttpUtils.isNetworkConnected(this)) {
            HttpUtils.get(Constant.CONTENT + entity.getId(), new TextHttpResponseHandler() { @Override public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) {

                } @Override public void onSuccess(int statusCode, Header[] headers, String responseString) {
                    SQLiteDatabase db = dbHelper.getWritableDatabase();
                    responseString = responseString.replaceAll("'", "''");
                    db.execSQL("replace into Cache(newsId,json) values(" + entity.getId() + ",'" + responseString + "')");
                    db.close();
                    parseJson(responseString);
                }
            });
        } else {
            SQLiteDatabase db = dbHelper.getReadableDatabase();
            Cursor cursor = db.rawQuery("select * from Cache where newsId = " + entity.getId(), null); if (cursor.moveToFirst()) {
                String json = cursor.getString(cursor.getColumnIndex("json"));
                parseJson(json);
            }
            cursor.close();
            db.close();
        }
这里将服务端返回的json串整体进行了保存,所以需要注意一下字符的转义问题。 
responseString = responseString.replaceAll("'", "''");  
这句很关键,如果不加,在文章内容中包含 时,会将sql语句隔断,导致应用崩溃。 
文章内容中的那些图片,WebView会自动帮我们缓存,所以我们在断网的情况下直接将html代码传入WebView就可以全部显示出来。 
今天的内容就这些,虽然看起来简单,但实际开发过程中会遇到各种各样的问题,要注意缓存的时机与缓存的清理。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值