markdownX是一个简单好用的md语法的app,作者不再维护,而且并不开源,尝试逆向看看有没有收获
基础情况
首先是markdownX的最新包1.1.1版本
download: MarkdownX-Official-1.1.1-release.apk.zip
使用的逆向工具是jeb
download: jeb225.zip
另一个逆向工具jadx
download: jadx-0.6.1.zip
先看一下manifest文件
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.ryeeeeee.markdownx" platformBuildVersionCode="22" platformBuildVersionName="5.1.1-1819727" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<supports-screens android:largeScreens="true" android:normalScreens="true" android:smallScreens="false" android:xlargeScreens="true" />
<uses-feature android:glEsVersion="0x20000" android:required="true" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<application android:allowBackup="true" android:icon="@drawable/ic_logo_48dp" android:label="@string/app_name" android:name="com.ryeeeeee.markdownx.App" android:theme="@style/AppTheme">
<meta-data android:name="UMENG_APPKEY" android:value="552a7743fd98c50c520013d3" />
<meta-data android:name="UMENG_CHANNEL" android:value="Official" />
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/app_name" android:name="com.ryeeeeee.markdownx.module.editor.EditorActivity" android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="file" />
<data android:host="*" />
<data android:pathPattern="/.*\\.md" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.ChooserActivity" android:theme="@style/AppTheme.TransparentStatusBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.AboutActivity" />
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.InformationActivity" />
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.module.settings.SettingsActivity" />
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.module.feedback.FeedbackActivity" android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity android:name="com.umeng.update.UpdateDialogActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:name="com.google.android.gms.ads.AdActivity" android:theme="@android:style/Theme.Translucent" />
<activity android:configChanges="keyboardHidden|orientation" android:exported="false" android:name="com.sina.weibo.sdk.component.WeiboSdkBrowser" android:windowSoftInputMode="adjustResize" />
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:name="com.ryeeeeee.markdownx.activity.ImagePreviewActivity">
<intent-filter>
<action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:configChanges="keyboard|orientation" android:launchMode="singleTask" android:name="com.dropbox.client2.android.AuthActivity">
<intent-filter>
<data android:scheme="db-yqgm7oie2uu9338" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="com.umeng.update.net.DownloadingService" android:process=":DownloadingService" />
<service android:name="com.ryeeeeee.markdownx.module.dropbox.UploadService" />
<provider android:authorities="com.ryeeeeee.markdownx.provider" android:exported="false" android:name="com.ryeeeeee.markdownx.module.compat.MarkdownContentProvider" />
<activity android:name="com.google.android.gms.ads.purchase.InAppPurchaseActivity" android:theme="@style/Theme.IAPTheme" />
<activity android:excludeFromRecents="true" android:exported="false" android:name="com.google.android.gms.auth.api.signin.internal.SignInHubActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<provider android:authorities="com.ryeeeeee.markdownx.google_measurement_service" android:exported="false" android:name="com.google.android.gms.measurement.AppMeasurementContentProvider" />
<receiver android:enabled="true" android:name="com.google.android.gms.measurement.AppMeasurementReceiver">
<intent-filter>
<action android:name="com.google.android.gms.measurement.UPLOAD" />
</intent-filter>
</receiver>
<service android:enabled="true" android:exported="false" android:name="com.google.android.gms.measurement.AppMeasurementService" />
</application>
</manifest>
看到入口Activity是 com.ryeeeeee.markdownx.activity.ChooserActivity
还可以看到打开手机里的.md文件使用的Activity是 com.ryeeeeee.markdownx.module.editor.EditorActivity
入口分析

打开ChooserActivity,在onCreate方法里看到布局文件是R.layout.activity_chooser
, 去到 activity_chooser.xml
里看到主要的布局是一个toolBar,一个FrameLayout
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/drawer_layout" app:fitsSystemWindows="true" app:layout_width="match_parent" app:layout_height="match_parent">
<RelativeLayout app:layout_width="match_parent" app:layout_height="match_parent">
<include app:id="@+id/tool_bar" layout="@layout/toolbar" />
<FrameLayout app:id="@+id/layout_main_panel" app:layout_width="match_parent" app:layout_height="match_parent" app:layout_below="@+id/tool_bar" />
</RelativeLayout>
<android.support.design.widget.NavigationView app:textColor="@color/black" app:layout_gravity="left|right|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/navigation" app:background="@color/white" app:fitsSystemWindows="true" app:layout_width="wrap_content" app:layout_height="match_parent" app:menu="@menu/menu_navigation" app:itemTextColor="@color/black_87_percent_alpha" app:headerLayout="@layout/navigation_header" app:insetForeground="#0000" />
</android.support.v4.widget.DrawerLayout>
回到代码里,根据FrameLayout的ID layout_main_panel
找到调用的地方:
private a k;
this.k = new e();
b_().a().b(R.id.layout_main_panel, this.k).a();
从这几个线索追踪,发现 a
是继承自Fragment
,e
又是继承自 a
所以,猜测 a
应该是类似 BaseFragment
的存在,而 e
应该是类似 ChooserFragment
接下来,继续看 e
包路径为 package com.ryeeeeee.markdownx.module.a
public class e extends a implements dc, n {
private static v ak;
private static String ap = "Local";
private static final String d = e.class.getSimpleName();
SwipeRefreshLayout a;
private GestureDetector aj;
private int al;
private boolean am = true;
private String an;
private String ao = "";
b b;
List c = new ArrayList();
private aa e;
private ListView f;
private l g;
private DrawShadowFrameLayout h;
private FloatingActionButton i;
从这里,我们可以看到用到了官方的下拉刷新组件SwipeRefreshLayout
,有用到手势监听处理GestureDetector
,列表ListView
与之对应的数据源应该是List c = new ArrayList()
,一个自定义(继承自FrameLayout)的容器组件DrawShadowFrameLayout
,一个官方的右下角悬浮按钮FloatingActionButton
。基本上就是这些,整体概念上来说比较清晰了。
通过View inflate = layoutInflater.inflate(R.layout.fragment_local, viewGroup, false)
找到fragment_local.xml
<?xml version="1.0" encoding="utf-8"?>
<com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/shadowLayout" app:layout_width="match_parent" app:layout_height="match_parent" app:topShadowDrawable="@drawable/header_shadow">
<android.support.v4.widget.SwipeRefreshLayout app:id="@+id/layout_swipe_refresh" app:layout_width="match_parent" app:layout_height="match_parent" app:layout_marginTop="@dimen/path_indicator_height">
<FrameLayout app:clickable="true" app:layout_width="match_parent" app:layout_height="match_parent">
<include app:id="@+id/empty_view" layout="@layout/empty_view" />
<ListView app:id="@+id/listview" app:background="@color/grey_300" app:layout_width="match_parent" app:layout_height="match_parent" app:divider="0x0" app:choiceMode="multipleChoiceModal" />
</FrameLayout>
</android.support.v4.widget.SwipeRefreshLayout>
<FrameLayout app:id="@+id/fragment_path_indicator" app:layout_width="match_parent" app:layout_height="match_parent" />
<com.getbase.floatingactionbutton.FloatingActionButton app:layout_gravity="top|bottom|left|right|center_vertical|fill_vertical|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/floating_action_button_create" app:layout_width="wrap_content" app:layout_height="wrap_content" app:layout_marginRight="@dimen/keyline_item_margin" app:layout_marginBottom="@dimen/keyline_item_margin" app:fab_colorPressed="@color/theme_dark_primary" app:fab_colorNormal="@color/theme_primary" app:fab_icon="@drawable/ic_create_white_24dp" />
</com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout>
我们来看主要功能
- 文档列表
- 新建按钮
使用Android studio看Log信息 比如过滤display
,会发现 列表item点击 和 新建按钮点击,都是跳转到EditorActivity
I/ActivityManager: Displayed com.ryeeeeee.markdownx/.module.editor.EditorActivity: +100ms
各种工具为我所用,然后我们重点关注文档列表相关的内容,因为文档列表item的点击和新建按钮点击走的逻辑基本相同。
看看列表Fragment —— e
假设别名: ChooserFragment
public final View a(LayoutInflater layoutInflater, ViewGroup viewGroup, Bundle bundle) {
View inflate = layoutInflater.inflate(R.layout.fragment_local, viewGroup, false);
ak = new v(this);
this.aj = new GestureDetector(this.e, new t());
this.a = (SwipeRefreshLayout) inflate.findViewById(R.id.layout_swipe_refresh);
this.a.setColorSchemeColors(h().getColor(R.color.theme_primary));
this.a.setOnRefreshListener(new i(this));
View findViewById = inflate.findViewById(R.id.empty_view);
findViewById.setOnTouchListener(new s());
this.f = (ListView) inflate.findViewById(R.id.listview);
this.f.setEmptyView(findViewById);
this.f.setOnItemClickListener(new j(this));
this.f.setMultiChoiceModeListener(new u());
this.f.setOnScrollListener(new k(this));
this.b = new b(this.e, this.f, this.c);
this.f.setAdapter(this.b);
this.f.setVerticalScrollBarEnabled(false);
this.f.setOnTouchListener(new l(this));
this.g = new l();
Bundle bundle2 = new Bundle();
bundle2.putString("extra_path", this.ao);
bundle2.putString("extra_display_root_name", ap);
this.g.e(bundle2);
this.g.a = this;
this.e.b_().a().b(R.id.fragment_path_indicator, this.g).a();
this.i = (FloatingActionButton) inflate.findViewById(R.id.floating_action_button_create);
this.i.setOnClickListener(new m(this));
this.h = (DrawShadowFrameLayout) inflate.findViewById(R.id.shadowLayout);
return inflate;
}
那么,我们关注ListView的信息,它的adapter是 com.ryeeeeee.markdownx.module.a.b extends BaseAdapter
,它的onItemClickListener
是 com.ryeeeeee.markdownx.module.a.j implements OnItemClickListener
我们来完整的看一看列表的适配器
package com.ryeeeeee.markdownx.module.a;
import android.content.Context;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.ryeeeeee.markdownx.R;
import com.ryeeeeee.markdownx.c.c;
import com.ryeeeeee.markdownx.c.g;
import de.hdodenhof.circleimageview.CircleImageView;
import java.io.File;
import java.util.List;
public class b extends BaseAdapter {
private static final String a = b.class.getSimpleName();
private Context b;
private List c;
private Typeface d = Typeface.createFromAsset(this.b.getAssets(), "Roboto-Light.ttf");
private Typeface e = Typeface.createFromAsset(this.b.getAssets(), "Roboto-Regular.ttf");
private ListView f;
public b(Context context, ListView listView, List list) {
this.b = context;
this.c = list;
this.f = listView;
}
public int getCount() {
return this.c.size();
}
public Object getItem(int i) {
return this.c.get(i);
}
public long getItemId(int i) {
return (long) i;
}
public View getView(int i, View view, ViewGroup viewGroup) {
d dVar;
if (view == null) {
view = LayoutInflater.from(this.b).inflate(R.layout.listview_item_folder_or_file, viewGroup, false);
dVar = new d();
dVar.a = view.findViewById(R.id.layout_item_root);
dVar.b = (TextView) view.findViewById(R.id.title);
dVar.d = (TextView) view.findViewById(R.id.size);
dVar.e = (TextView) view.findViewById(R.id.modified_time);
dVar.f = (CircleImageView) view.findViewById(R.id.image);
dVar.c = view.findViewById(R.id.divider);
view.setTag(dVar);
} else {
dVar = (d) view.getTag();
}
File file = (File) this.c.get(i);
boolean isFile = file.isFile();
boolean isItemChecked = this.f.isItemChecked(i);
dVar.b.setText(file.getName());
dVar.b.setTypeface(this.e);
dVar.d.setTypeface(this.d);
if (isFile) {
new StringBuilder("getView file:").append(file.getName()).append(" file");
if (isItemChecked) {
dVar.f.setImageResource(R.drawable.ic_file_dark_40dp);
} else {
dVar.f.setImageResource(R.drawable.ic_file_light_40dp);
}
dVar.d.setText(c.c(file));
} else {
if (isItemChecked) {
dVar.f.setImageResource(R.drawable.ic_folder_dark_40dp);
} else {
dVar.f.setImageResource(R.drawable.ic_folder_light_40dp);
}
new StringBuilder("getView file:").append(file.getName()).append(" directory");
dVar.d.setText(R.string.folder);
}
dVar.e.setTypeface(this.d);
dVar.e.setText(g.a(this.b, file.lastModified()));
dVar.f.setOnClickListener(new c(this, i));
if (isItemChecked) {
dVar.a.setBackgroundResource(R.drawable.bg_listview_item_selected);
dVar.b.setTextColor(this.b.getResources().getColor(R.color.white));
dVar.d.setTextColor(this.b.getResources().getColor(R.color.white_54_percent_alpha));
dVar.e.setTextColor(this.b.getResources().getColor(R.color.white_54_percent_alpha));
dVar.c.setBackgroundResource(R.color.white);
} else {
dVar.a.setBackgroundResource(R.drawable.bg_listview_item);
dVar.b.setTextColor(this.b.getResources().getColor(R.color.black_87_percent_alpha));
dVar.d.setTextColor(this.b.getResources().getColor(R.color.theme_secondary_text));
dVar.e.setTextColor(this.b.getResources().getColor(R.color.theme_secondary_text));
dVar.c.setBackgroundResource(R.color.theme_divider);
}
if (i == this.c.size() - 1) {
dVar.c.setVisibility(4);
} else {
dVar.c.setVisibility(0);
}
return view;
}
}
每一个item的布局文件listview_item_folder_or_file.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/layout_item_root" app:background="@color/white" app:paddingLeft="16dp" app:paddingTop="16dp" app:paddingRight="16dp" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x4801">
<de.hdodenhof.circleimageview.CircleImageView app:id="@id/image" app:layout_width="UNKNOWN_DATA_0x2801" app:layout_height="UNKNOWN_DATA_0x2801" app:layout_marginRight="16dp" app:src="@color/theme_primary" app:layout_centerVertical="true" app:civ_fill_color="@color/white" />
<TextView app:textSize="16sp" app:textColor="@color/black_87_percent_alpha" app:gravity="top|bottom|center_vertical|fill_vertical|center|fill" app:id="@id/title" app:layout_width="match_parent" app:layout_height="wrap_content" app:singleLine="true" app:layout_toRightOf="@id/image" />
<TextView app:textSize="14sp" app:gravity="top|bottom|left|right|center_vertical|fill_vertical|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/modified_time" app:layout_width="wrap_content" app:layout_height="match_parent" app:layout_alignTop="@+id/size" app:layout_alignBottom="@+id/size" app:layout_alignParentRight="true" />
<TextView app:textSize="14sp" app:typeface="sans" app:textColor="@color/theme_secondary_text" app:id="@+id/size" app:layout_width="match_parent" app:layout_height="wrap_content" app:singleLine="true" app:layout_toLeftOf="@+id/modified_time" app:layout_above="@+id/divider" app:layout_alignLeft="@id/title" />
<View app:id="@+id/divider" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x7f080067" app:layout_marginLeft="56dp" app:layout_marginTop="15dp" app:layout_alignParentBottom="true" style="@style/horizontal_divider" />
</RelativeLayout>
Adapter的数据源是一个List,找到相关的内容
private List c;
File file = (File) this.c.get(i);
dVar.b.setText(file.getName());
dVar.e.setText(g.a(this.b, file.lastModified()));
dVar.d.setText(c.c(file)); // 如果是文件,显示文件size
dVar.d.setText(R.string.folder); // 如果是文件夹,显示“文件夹”三个字
推理得知数据源为List<File>
,目前逆向工具看起来是看不了泛型。也就是说,列表上面显示的内容其实是文件的一些信息:文件名、文件大小、文件最后更新时间
数据源来自
sdcard/Android/data/com.ryeeeeee.markdownx/files/notes/

这样,与列表展示的实际情况就对应上了。
接下来是item的点击 setOnItemClickListener(new j(this))
,完整代码如下
package com.ryeeeeee.markdownx.module.a;
import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Build.VERSION;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import com.ryeeeeee.markdownx.R;
import com.ryeeeeee.markdownx.module.editor.EditorActivity;
import java.io.File;
final class j implements OnItemClickListener {
final /* synthetic */ e a;
j(e eVar) {
this.a = eVar;
}
public final void onItemClick(AdapterView adapterView, View view, int i, long j) {
File file = (File) this.a.c.get(i);
if (file.isDirectory()) {
this.a.g.a(file.getName());
return;
}
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_file_path", file.getAbsolutePath());
intent.putExtra("extra_launch_type", 2);
if (VERSION.SDK_INT >= 21) {
this.a.e.startActivityForResult(intent, 0, ActivityOptions.makeSceneTransitionAnimation(this.a.e, view, this.a.h().getString(R.string.shared_element_article)).toBundle());
return;
}
this.a.e.startActivityForResult(intent, 0);
}
}
顺便看一下右下角浮动按钮的点击
this.i = (FloatingActionButton) inflate.findViewById(R.id.floating_action_button_create);
this.i.setOnClickListener(new m(this));
final class m implements OnClickListener {
final /* synthetic */ e a;
m(e eVar) {
this.a = eVar;
}
public final void onClick(View view) {
e.d;
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_directory", this.a.an + this.a.ao);
intent.putExtra("extra_launch_type", 4);
this.a.a(intent, 0);
}
}
那么,这里看到两种启动EditorActivity
的方式
// ListView item点击
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_file_path", file.getAbsolutePath());
intent.putExtra("extra_launch_type", 2);
// FloatingActionButton点击
Intent intent = new Intent(this.a.e, EditorActivity.class);
intent.putExtra("extra_directory", this.a.an + this.a.ao);
intent.putExtra("extra_launch_type", 4);
内容编辑页EditorActivity
内容编辑与预览,这部分是markdownX的核心功能,内容相对来说多一些,不过不要慌,一步步推进。
先找到EditorActivity的onCreate方法,看到使用的是activity_edit.xml
的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res-auto" app:orientation="vertical" app:background="@color/white" app:fitsSystemWindows="true" app:layout_width="match_parent" app:layout_height="match_parent">
<include layout="@layout/toolbar" />
<com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout app:id="@+id/shadowLayout" app:background="@color/white" app:layout_width="match_parent" app:layout_height="match_parent" app:topShadowDrawable="@drawable/header_shadow">
<android.support.v4.view.ViewPager app:id="@+id/view_pager_switch" app:layout_width="match_parent" app:layout_height="match_parent" />
<include app:id="@+id/layout_shortcut" app:visibility="gone" layout="@layout/shortcuts" />
<com.ryeeeeee.markdownx.widget.ShadowCircleIndeterminateProgressBar app:layout_gravity="left|right|center_horizontal|fill_horizontal|center|fill|start|end" app:id="@+id/progressBar" app:visibility="gone" app:layout_width="UNKNOWN_DATA_0x2401" app:layout_height="UNKNOWN_DATA_0x2401" app:layout_marginTop="100dp" />
</com.ryeeeeee.markdownx.widget.DrawShadowFrameLayout>
</LinearLayout>
结合markdownX的使用体验,编辑页的左侧编辑+右侧预览的模式,自然联想到viewpager,在布局文件中也看到了ViewPager。
我们先顺着这条线索去寻找。在onCreate方法中看到:
this.t = new al();
this.t.g = this;
this.u = new az();
this.u.e = this;
this.o = (ViewPager) findViewById(R.id.view_pager_switch);
if (this.o != null) {
this.v = com.ryeeeeee.markdownx.data.b.a;
this.q.add(0, this.t);
this.q.add(1, this.u);
this.o.setOnPageChangeListener(new aj());
this.o.setAdapter(new ak(this, b_(), this.q));
boolean z = this.p == 6 || this.p == 2 || this.p == 1;
if (z) {
this.o.setCurrentItem(1);
} else {
this.o.setCurrentItem(0);
}
} else {
this.v = com.ryeeeeee.markdownx.data.b.b;
android.support.v4.app.ai a = b_().a();
a.b(R.id.fragment_editor, this.t);
a.b(R.id.fragment_preview, this.u);
a.a();
}
我的解读是:ViewPager的Adapter包含两个元素,一个是 al
类型的Fragment(R.id.fragment_editor, this.t)
, 一个是 az
类型的Fragment(R.id.fragment_preview, this.u)
接下来分两个分支去追踪
Fragment_editor : al类型
com.ryeeeeee.markdownx.module.editor
包的 class al extends Fragment
对应的布局文件是fragment_editor.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layout_pager_editor" android:background="@color/white" android:layout_width="match_parent" android:layout_height="match_parent">
<com.ryeeeeee.markdownx.widget.ObservableScrollView android:id="@+id/scroll_for_editor" android:layout_width="match_parent" android:layout_height="match_parent">
<LinearLayout android:orientation="vertical" android:id="@+id/inner_of_scroller" android:layout_width="match_parent" android:layout_height="wrap_content">
<EditText android:textSize="16dp" android:textColor="@color/black_87_percent_alpha" android:gravity="top|bottom|center_vertical|fill_vertical|center|fill" android:id="@+id/edit_text_title" android:background="@color/white" android:paddingLeft="@dimen/keyline_item_margin" android:paddingRight="@dimen/keyline_item_margin" android:layout_width="match_parent" android:layout_height="UNKNOWN_DATA_0x3801" android:hint="title" android:singleLine="true" />
<View style="@style/horizontal_divider" />
<EditText android:textColor="@color/grey_800" android:gravity="top|bottom|center_vertical|fill_vertical|center|fill" android:id="@+id/edit_text_content" android:background="@color/white" android:layout_width="match_parent" android:layout_height="match_parent" android:hint="compose" android:minLines="10" android:lineSpacingExtra="@dimen/editor_line_space_extra" style="@style/multiline_text_field" />
</LinearLayout>
</com.ryeeeeee.markdownx.widget.ObservableScrollView>
</RelativeLayout>
包含两个EditText,第一个是md文件的title,第二个是md文件的内容,中间是分割线。
在代码中
EditText b;
EditText c;
this.c = (EditText) inflate.findViewById(R.id.edit_text_content);
this.c.setTypeface(this.i);
this.b = (EditText) inflate.findViewById(R.id.edit_text_title);
this.b.setTypeface(this.i);
this.b.setOnFocusChangeListener(new an(this));
看到这里,也许你觉得这里没有什么内容,其实不然。编辑页顶部的工具栏(markdown语法快捷工具栏)跟这个EditText是密切相关的。我们需要回过头去看EditorActivity
,在EditorActivity
的onCreate方法里有一行不经意的代码
n();
而这个方法就是创建markdown工具栏并赋予编辑功能
private void n() {
this.y = getResources().getDimensionPixelSize(R.dimen.shortcut_group_height);
this.n = (ViewGroup) findViewById(R.id.layout_shortcut);
this.m = (ViewGroup) findViewById(R.id.shortcut_container);
int[] iArr = new int[]{R.integer.action_insert_bulleted_list, R.integer.action_insert_order_list, R.integer.action_insert_link, R.integer.action_insert_image, R.integer.action_insert_code, R.integer.action_insert_bold, R.integer.action_insert_header1, R.integer.action_insert_header2, R.integer.action_insert_header3, R.integer.action_insert_quotes, R.integer.action_insert_inline_code, R.integer.action_insert_horizontal, R.integer.action_insert_strikethrough, R.integer.action_insert_table, R.integer.action_insert_header4, R.integer.action_insert_header5, R.integer.action_insert_header6};
for (int i = 0; i < 17; i++) {
int i2 = iArr[i];
ImageButton imageButton = (ImageButton) getLayoutInflater().inflate(R.layout.button_shortcut, this.m, false);
this.m.addView(imageButton);
switch (i2) {
case R.integer.action_insert_bold:
imageButton.setImageResource(R.drawable.ic_format_bold_white_24dp);
imageButton.setOnClickListener(new s(this));
break;
case R.integer.action_insert_bulleted_list:
imageButton.setImageResource(R.drawable.ic_format_list_bulleted_white_24dp);
imageButton.setOnClickListener(new l(this));
break;
case R.integer.action_insert_code:
imageButton.setImageResource(R.drawable.ic_console_white_24dp);
imageButton.setOnClickListener(new r(this));
break;
case R.integer.action_insert_header1:
imageButton.setImageResource(R.drawable.ic_format_header_1_white_24dp);
imageButton.setOnClickListener(new f(this));
break;
case R.integer.action_insert_header2:
imageButton.setImageResource(R.drawable.ic_format_header_2_white_24dp);
imageButton.setOnClickListener(new g(this));
break;
case R.integer.action_insert_header3:
imageButton.setImageResource(R.drawable.ic_format_header_3_white_24dp);
imageButton.setOnClickListener(new h(this));
break;
case R.integer.action_insert_header4:
imageButton.setImageResource(R.drawable.ic_format_header_4_white_24dp);
imageButton.setOnClickListener(new i(this));
break;
case R.integer.action_insert_header5:
imageButton.setImageResource(R.drawable.ic_format_header_5_white_24dp);
imageButton.setOnClickListener(new j(this));
break;
case R.integer.action_insert_header6:
imageButton.setImageResource(R.drawable.ic_format_header_6_white_24dp);
imageButton.setOnClickListener(new k(this));
break;
case R.integer.action_insert_horizontal:
imageButton.setImageResource(R.drawable.ic_minus_white_24dp);
imageButton.setOnClickListener(new ac(this));
break;
case R.integer.action_insert_image:
imageButton.setImageResource(R.drawable.ic_insert_photo_white_24dp);
imageButton.setOnClickListener(new o(this));
break;
case R.integer.action_insert_inline_code:
imageButton.setImageResource(R.drawable.ic_xml_white_24dp);
imageButton.setOnClickListener(new e(this));
break;
case R.integer.action_insert_link:
imageButton.setImageResource(R.drawable.ic_insert_link_white_24dp);
imageButton.setOnClickListener(new m(this));
break;
case R.integer.action_insert_order_list:
imageButton.setImageResource(R.drawable.ic_format_list_numbers_white_24dp);
imageButton.setOnClickListener(new ae(this));
break;
case R.integer.action_insert_quotes:
imageButton.setImageResource(R.drawable.ic_format_quote_white_24dp);
imageButton.setOnClickListener(new ad(this));
break;
case R.integer.action_insert_strikethrough:
imageButton.setImageResource(R.drawable.ic_format_strikethrough_white_24dp);
imageButton.setOnClickListener(new af(this));
break;
case R.integer.action_insert_table:
imageButton.setImageResource(R.drawable.ic_grid_white_24dp);
imageButton.setOnClickListener(new ab(this));
break;
default:
break;
}
}
}
这里有17个ImageButton,而实际上markdownX的工具栏就是17个button。那么这里的switch...case给每一个按钮赋予了自己的onClickListerner。比如我们看一下action_insert_code
,也就是markdown的插入代码
final class r implements OnClickListener {
final /* synthetic */ EditorActivity a;
r(EditorActivity editorActivity) {
this.a = editorActivity;
}
public final void onClick(View view) {
al e = this.a.t;
String obj = e.c.getText().toString();
int selectionStart = e.c.getSelectionStart();
int selectionEnd = e.c.getSelectionEnd();
String substring = obj.substring(selectionStart, selectionEnd);
obj = obj.substring(0, selectionStart);
int lastIndexOf = obj.lastIndexOf(10);
if (lastIndexOf != -1) {
obj = obj.substring(lastIndexOf + 1);
}
Object obj2;
if (e.a(obj)) {
obj2 = "```\n" + substring + "\n```\n";
e.c.getText().replace(lastIndexOf + 1, selectionEnd, obj2);
e.c.setSelection((obj2.length() + (lastIndexOf + 1)) - 5);
} else {
obj2 = "\n```\n" + substring + "\n```\n";
e.c.getText().replace(selectionStart, selectionEnd, obj2);
e.c.setSelection((obj2.length() + selectionStart) - 5);
}
this.a.o();
}
}
注意这行代码
al e = this.a.t;
String obj = e.c.getText().toString();
a是EditorActivity,t是什么? 翻看EditorActivity代码,t就是al,而al就是Fragment_editor
好了,变量e就是编辑Fragment,那么接下来e.c是什么?
this.c = (EditText) inflate.findViewById(R.id.edit_text_content);
e.c就是EditorFragment里面的内容EditText
下面的逻辑就是在你选中的内容前后加入"\n```\n"
,这是markdown的标准语法,其他的onClickListener也是类似的思路,去实现自己标签的特定语法。
整理一下思路:在EditorActivity点击按钮,操作EditorFragment里面的EditText加入指定的字符串来包裹。
接下来,我们来看另一个分支
Fragment_preview : az类型
com.ryeeeeee.markdownx.module.editor
包里的class az extends Fragment
布局文件是fragment_preview.xml
<?xml version="1.0" encoding="utf-8"?>
<com.ryeeeeee.markdownx.widget.ObservableScrollView xmlns:android="http://schemas.android.com/apk/res-auto" app:id="@+id/scroll_for_preview" app:layout_width="match_parent" app:layout_height="match_parent" app:fillViewport="true">
<LinearLayout app:orientation="vertical" app:id="@+id/inner_of_scroller" app:layout_width="match_parent" app:layout_height="match_parent">
<RelativeLayout app:layout_width="match_parent" app:layout_height="match_parent">
<TextView app:textSize="16dp" app:textColor="@color/black_87_percent_alpha" app:gravity="top|bottom|center_vertical|fill_vertical|center|fill" app:id="@+id/text_title" app:background="@color/white" app:paddingLeft="@dimen/keyline_item_margin" app:paddingRight="@dimen/keyline_item_margin" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x3801" app:singleLine="true" />
<View app:id="@+id/divider" app:layout_below="@+id/text_title" style="@style/horizontal_divider" />
<com.ryeeeeee.markdownx.module.editor.MarkdownPreviewView app:id="@+id/web_view_preview" app:layout_width="match_parent" app:layout_height="wrap_content" app:layout_below="@+id/divider" />
<com.google.android.gms.ads.AdView app:id="@+id/adView" app:visibility="gone" app:layout_width="match_parent" app:layout_height="UNKNOWN_DATA_0x7f080051" app:layout_alignParentBottom="true" app:adSize="BANNER" app:adUnitId="@string/admob_preview_ad_unit_id" />
</RelativeLayout>
</LinearLayout>
</com.ryeeeeee.markdownx.widget.ObservableScrollView>
核心内容是com.ryeeeeee.markdownx.module.editor.MarkdownPreviewView
,直接来看MarkdownPreviewView
的完整代码
package com.ryeeeeee.markdownx.module.editor;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
public class MarkdownPreviewView extends LinearLayout {
private static final String b = MarkdownPreviewView.class.getSimpleName();
WebView a;
private Context c;
private ay d;
private av e;
public MarkdownPreviewView(Context context) {
super(context);
a(context);
}
public MarkdownPreviewView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
a(context);
}
public MarkdownPreviewView(Context context, AttributeSet attributeSet, int i) {
super(context, attributeSet, i);
a(context);
}
@SuppressLint({"AddJavascriptInterface", "SetJavaScriptEnabled"})
private void a(Context context) {
if (!isInEditMode()) {
this.c = context;
setOrientation(1);
if (VERSION.SDK_INT >= 21) {
WebView.enableSlowWholeDocumentDraw();
}
this.a = new WebView(this.c);
this.a.getSettings().setJavaScriptEnabled(true);
this.a.setVerticalScrollBarEnabled(false);
this.a.setHorizontalScrollBarEnabled(false);
this.a.addJavascriptInterface(new aw(), "handler");
this.a.setWebViewClient(new ax());
this.a.loadUrl("file:///android_asset/markdown.html");
addView(this.a, new LayoutParams(-1, -1));
}
}
public final void a(String str, boolean z) {
this.a.loadUrl("javascript:parseMarkdown(\"" + str.replace("\n", "\\n").replace("\"", "\\\"").replace("'", "\\'") + "\", " + z + ")");
}
public void setContentListener(av avVar) {
this.e = avVar;
}
public void setOnLoadingFinishListener(ay ayVar) {
this.d = ayVar;
}
}
这里面的核心内容是使用WebView加载"file:///android_asset/markdown.html"
这个HTML,也就是说,我们写好的md文件是通过html+js来解析,在WebView里面展示出来。
导出html和js文件
看一下html文件
<script src="marked.min.js"></script>
</head>
<body>
<article id="content" class="markdown-body"></article>
<script type="text/javascript">
function parseMarkdown(content, gfmEnabled) {
marked.setOptions({
renderer: new marked.Renderer(),
gfm: gfmEnabled,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
document.getElementById('content').innerHTML = marked(content);
parseDone();
}
function parseDone() {
window.handler.onHTMLGenerated();
}
</script>
</body>
外界通过调用MarkdownPreviewView
的方法是调用js中的function parseMarkdown
来传入需要解析的markdown语法的字符串
public final void a(String str, boolean z) {
this.a.loadUrl("javascript:parseMarkdown(\"" + str.replace("\n", "\\n").replace("\"", "\\\"").replace("'", "\\'") + "\", " + z + ")");
}
总结:
markdown的编辑:在EditorActivity点击按钮,操作EditorFragment里面的EditText加入指定的字符串来包裹。
markdown的预览:在PreviewFragment通过js解析编辑好的md文件,使用可以自定义的css样式来展示。
网友评论