Android高仿知乎日报(下)


Part4


文章の内容の展示や、CoordinatorLayoutなど一連のデザインパッケージのコントロールでスクロール効果を実現したり、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ディレクトリに直接配置されます.loadData WithBase URLでロードすることを覚えておいてください.直接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の全画面拡散アニメーションを再生し、再生後、すべてのコントロールを表示することで、クリック先から徐々に拡散するインタフェースの仮象となります. 

Part5


今では高仿知乎日报の基本的な読解机能が実现していますが、ただ読みたいだけなら十分ですが、高仿である以上、できるだけ多くの机能を作っておきましょう. 
主に夜間モードの機能をしていますが、実は肌を変える機能で、昼間と夜間の2つのモードしかないので、ネット上に伝わる皮膚の方案も必要ありません.背景色とフォントの色を直接更新すればいいです.まず使用するすべての色を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メソッドは2つの言葉しかありませんが、肝心なところはどこですか?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の値を取得し、コードで夜間モードかどうかを判断して背景色や文字色を動的に変更していることがわかります.原理はこのようにして、その他のは贅沢に述べません. 

Part6


先日、ほとんどの機能が完了しましたが、今日はオフラインキャッシュという実用的な機能を追加します.キャッシュを付けない前に、ネットワークを接続していない状態でアプリケーションを開くと、横滑りしたメニューでも効果が表示されず、インタフェースの文章リストや文章を読むと表示されなくなります.では、キャッシュを少しずつ追加しましょう.  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をkey、json文字列をvalueとしてSharedPreferencesに格納します.ニュースタイプを取得する要求は1つしかないので、データベースで格納する必要はありません.文章リストと文章内容を後でキャッシュするときにデータライブラリを使用します.  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に直接転送すればすべて表示できます.シンプルに見えますが、実際の開発では様々な問題が発生します.キャッシュのタイミングとキャッシュのクリーンアップに注意してください.