高仿知乎日報(一)
之前寫的高仿知乎日報的程式碼是用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地址
最後歡迎大家到我的個人部落格訪問與評論。