package com.gracecode.iZhihu.fragment; import android.animation.Animator; import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Picture; import android.os.Message; import android.preference.PreferenceManager; import android.text.Html; import android.util.Base64; import android.util.Log; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.webkit.*; import com.gracecode.iZhihu.R; import com.gracecode.iZhihu.dao.Question; import com.gracecode.iZhihu.db.ThumbnailsDatabase; import com.gracecode.iZhihu.util.Helper; import taobe.tec.jcc.JChineseConvertor; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DetailFragment extends WebViewFragment { private static final String TAG = DetailFragment.class.getName(); private static final String KEY_SCROLL_BY = "key_scroll_by_"; private static final String TEMPLATE_DETAIL_FILE = "detail.html"; private static final String URL_ASSETS_PREFIX = "file:///android_asset/"; private static final String MIME_HTML_TYPE = "text/html"; public static final int ID_NOT_FOUND = 0; private static final int FIVE_MINUTES = 1000 * 60 * 5; private static final String NEED_CONVERT = "0x000"; private static final String NONEED_CONVERT = "0x111"; private static final int INITIAL_VIEW = 0x00; private static final int RESUME_JUMP = 0x01; private final Context context; private Activity activity; private static ThumbnailsDatabase thumbnailsDatabase; private SharedPreferences sharedPreferences; private static JChineseConvertor chineseConvertor = null; private Question question; private boolean isNeedIndent = false; private boolean isNeedReplaceSymbol = false; private boolean isNeedCacheThumbnails = true; private boolean isShareByTextOnly = false; private boolean isNeedConvertTraditionalChinese = false; private boolean isCustomFontEnabled = false; private String customFontPath; private String customBoldFontPath; /** * 刷新 UI 线程集中的地方 */ private final android.os.Handler UIChangedChangedHandler = new android.os.Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case INITIAL_VIEW: if (getWebView() != null) { getWebView().setVisibility(View.VISIBLE); } new Timer().schedule(new TimerTask() { @Override public void run() { if (getWebView() != null) { UIChangedChangedHandler.sendEmptyMessage(RESUME_JUMP); } } }, 1000); break; case RESUME_JUMP: if (getWebView() != null) { getWebView().setVisibility(View.VISIBLE); getWebView().setScrollY(getSavedScrollYOffset()); } break; } } }; private void saveScrollYOffset() { SharedPreferences.Editor edit = sharedPreferences.edit(); edit.putInt(getKeyScrollById(), getWebView().getScrollY()); edit.commit(); } private int getSavedScrollYOffset() { return sharedPreferences.getInt(getKeyScrollById(), 0); } private final WebViewClient webViewClient = new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (Helper.isExternalStorageExists() && !isShareByTextOnly) { new Thread(genScreenShots).start(); } getWebView().pageDown(true); UIChangedChangedHandler.sendEmptyMessage(INITIAL_VIEW); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Helper.openWithBrowser(getActivity(), url); return true; } }; private final Runnable genScreenShots = new Runnable() { @Override public void run() { Bitmap bitmap = null; try { Thread.sleep(2000); if (!isTempScreenShotsFileCached()) { File screenShotsFile = getTempScreenShotsFile(); FileOutputStream fileOutPutStream = new FileOutputStream(screenShotsFile); bitmap = genCaptureBitmap(); if (bitmap != null && !bitmap.isRecycled()) { bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutPutStream); fileOutPutStream.flush(); fileOutPutStream.close(); Log.d(TAG, "Generated screenshots at " + screenShotsFile.getAbsolutePath()); } } } catch (IOException e) { e.printStackTrace(); } catch (OutOfMemoryError e) { // @todo mark do not generate again. e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } } } }; public boolean isTempScreenShotsFileCached() { File tempScreenShotsFile = getTempScreenShotsFile(); Boolean is5MinutesAgo = (System.currentTimeMillis() - tempScreenShotsFile.lastModified()) < FIVE_MINUTES; return tempScreenShotsFile.exists() && tempScreenShotsFile.length() > 0 && is5MinutesAgo; } public File getTempScreenShotsFile() { return new File(context.getCacheDir(), question.getAnswerId() + ".png"); } private String getTemplateString() { String template; try { template = Helper.getFileContent(activity.getAssets().open(TEMPLATE_DETAIL_FILE)); } catch (IOException e) { return "%s"; } return template; } public DetailFragment(Context context, Question question) { this.context = context; this.question = question; try { chineseConvertor = JChineseConvertor.getInstance(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onResume() { super.onResume(); this.activity = getActivity(); this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); thumbnailsDatabase = new ThumbnailsDatabase(context); this.isNeedIndent = sharedPreferences.getBoolean(getString(R.string.key_indent), false); this.isNeedReplaceSymbol = sharedPreferences.getBoolean(getString(R.string.key_symbol), true); this.isNeedCacheThumbnails = sharedPreferences.getBoolean(getString(R.string.key_enable_cache), true); this.isShareByTextOnly = sharedPreferences.getBoolean(getString(R.string.key_share_text_only), false); this.isNeedConvertTraditionalChinese = sharedPreferences.getBoolean(getString(R.string.key_traditional_chinese), false); // Custom Fonts Preference this.isCustomFontEnabled = sharedPreferences.getBoolean(getString(R.string.key_custom_fonts_enabled), false); this.customFontPath = sharedPreferences.getString(getString(R.string.key_custom_fonts), ""); this.customBoldFontPath = sharedPreferences.getString(getString(R.string.key_custom_fonts_bold), ""); WebView webView = getWebView(); WebSettings webSettings = webView.getSettings(); webSettings.setLoadWithOverviewMode(true); webSettings.setUseWideViewPort(true); webSettings.setJavaScriptEnabled(false); // Load page from generated HTML string. webView.loadDataWithBaseURL(URL_ASSETS_PREFIX, getFormatedContent(), MIME_HTML_TYPE, Helper.DEFAULT_CHARSET, null); webView.setWebViewClient(webViewClient); webView.setWebChromeClient(new WebChromeClient() { public boolean onConsoleMessage(ConsoleMessage cm) { Log.d(TAG, cm.message() + "\nFrom line " + cm.lineNumber() + " of " + cm.sourceId()); return true; } }); webView.setVisibility(View.INVISIBLE); } private File getCachedFile() { String CONVERT_FLAG = isNeedConvertTraditionalChinese ? NEED_CONVERT : NONEED_CONVERT; String hashed = Base64.encodeToString((question.getId() + CONVERT_FLAG + getClassName()).getBytes(), Base64.DEFAULT); return new File(activity.getCacheDir(), hashed.trim()); } private String getFormatedContent() { String className = getClassName(), templateString = getTemplateString(), data = ""; // Cached by file. try { File cacheFile = getCachedFile(); // @TODO Cache needed. if (false && cacheFile.exists()) { data = Helper.getFileContent(cacheFile.getAbsolutePath()); } else { if (!isCustomFontEnabled) { this.customFontPath = ""; this.customBoldFontPath = ""; } data = String.format(templateString, "file:///" + customFontPath, "file:///" + customBoldFontPath, className, isNeedReplaceSymbol ? Helper.replaceSymbol(question.getTitle()) : question.getTitle(), formatContent(question.getDescription()), question.getUserName(), "<p class='update-at'>" + question.getUpdateAt() + "</p>" + formatContent(question.getContent()) ); // @see https://code.google.com/p/jcc/ data = (isNeedConvertTraditionalChinese) ? chineseConvertor.s2t(data) : chineseConvertor.t2s(data); // util.putFileContent(cacheFile, new ByteArrayInputStream(data.getBytes())); } } catch (IOException e) { e.printStackTrace(); } finally { return data; } } public String getPlainContent() { return Html.fromHtml(getFormatedContent()).toString(); } private String getKeyScrollById() { return KEY_SCROLL_BY + question.getId(); } @Override public void onPause() { super.onPause(); saveScrollYOffset(); } private String formatParagraph(String content) { Pattern pattern = Pattern.compile("<hr>"); Matcher matcher = pattern.matcher(content); content = matcher.replaceAll("</p><p>"); pattern = Pattern.compile("<p>\\s+"); matcher = pattern.matcher(content); content = matcher.replaceAll("<p>"); pattern = Pattern.compile("<p></p>"); matcher = pattern.matcher(content); content = matcher.replaceAll(""); content = "<p>" + content + "</p>"; return content; } private String formatContent(String content) { content = formatParagraph(content); if (isNeedReplaceSymbol) { content = Helper.replaceSymbol(content); } if (isNeedCacheThumbnails) { List<String> imageUrls = Helper.getImageUrls(content); for (String url : imageUrls) { if (thumbnailsDatabase.isCached(url)) { String localPathString = thumbnailsDatabase.getCachedPath(url); Log.v(TAG, "Found offline cache file, replace online image " + url + " with file://" + localPathString); content = content.replace(url, "file://" + localPathString); } else { thumbnailsDatabase.add(url); } } } return content; } private String getClassName() { String className = ""; int fontSize = Integer.parseInt( sharedPreferences.getString(getString(R.string.key_font_size), getString(R.string.default_font_size))); switch (fontSize) { case 12: className += " tiny"; break; case 14: className += " small"; break; case 16: className += " normal"; break; case 18: className += " big"; break; case 20: className += " huge"; break; default: className += " normal"; } if (isNeedIndent) { className += " indent"; } if (isCustomFontEnabled) { className += " custom-fonts"; } return className; } /** * 截取所有网页内容到 Bitmap * * @return Bitmap */ Bitmap genCaptureBitmap() throws OutOfMemoryError { // @todo Future versions of WebView may not support use on other threads. try { Picture picture = getWebView().capturePicture(); int height = picture.getHeight(), width = picture.getWidth(); if (height == 0 || width == 0) { return null; } Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); picture.draw(canvas); return bitmap; } catch (NullPointerException e) { return null; } } private int getPageScrollHeight() { return getWebView().getHeight() / 2; } boolean isPaging = false; synchronized public void scrollPageBy(int offset) { if (isPaging) return; int contentHeight = (int) (getWebView().getContentHeight() * getWebView().getScale()); int maxScrollHeight = contentHeight - getWebView().getHeight(); int scrollY = getWebView().getScrollY() + offset; if (scrollY < 0) { scrollY = 0; } if (scrollY >= maxScrollHeight) { scrollY = maxScrollHeight; } ObjectAnimator animator = ObjectAnimator.ofInt(getWebView(), "scrollY", scrollY); animator.setInterpolator(new DecelerateInterpolator()); animator.setDuration(250); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { isPaging = true; } @Override public void onAnimationEnd(Animator animator) { isPaging = false; } @Override public void onAnimationCancel(Animator animator) { isPaging = false; } @Override public void onAnimationRepeat(Animator animator) { } }); animator.start(); // getWebView().scrollTo(0, scrollY); } public void nextPage() { scrollPageBy(getPageScrollHeight()); } public void prevPage() { scrollPageBy(getPageScrollHeight() * -1); } }