1. 程式人生 > >高仿知乎日報(一)

高仿知乎日報(一)

之前寫的高仿知乎日報的程式碼是用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上看完整程式碼,我會根據進度實時上傳。
github地址
最後歡迎大家到我的個人部落格訪問與評論。