Итак, для приготовления "блокнота с подсветкой синтаксиса" в Android нам понадобится:
- Хорошая библиотека для подсветки синтаксиса на JavaScript+CSS. Можно выбрать тут, мне больше всего понравилась CodeMirror, её и будем использовать.
- WebView для интеграции всего этого богатства с нашим приложением
- Немного кода, для взаимодействия с WebView и работающим в нём JavaScript.
Делаем локальную web-страницу и включаем в неё библиотеку CodeMirror
Код страницы:
<html> <head> <link rel="stylesheet" href="codemirror.css"><link> <script type="text/javascript" src="codemirror.js"></script> <script type="text/javascript" src="clike.js"></script> <link rel="stylesheet" href="docs.css"><link> <style> body { margin: 0; padding: 0; } .CodeMirror-scroll { height: auto; overflow-y: hidden; overflow-x: auto; width: 100%; } </style> </head> <body> <form> <textarea id="code" name="code" style="width: 100%; height: 100%"></textarea> </form> <script type="text/javascript"> var delay; var editor = CodeMirror.fromTextArea(document.getElementById("code"), { lineNumbers : true, matchBrackets : true, mode : "text/x-java", onChange: function() { clearTimeout(delay); delay = setTimeout(updateRez, 300); } }); function updateRez() { Android.contOut(editor.getValue()); } function setContent(content) { editor.setValue(decodeURIComponent(content)); } delay = setTimeout(updateRez, 300); </script> </body> </html>
Тут мы подключаем два js-файла: ядро codemirror и файл с форматированием для "c-like" языков (с, java и т.п.). Тут же используем css-файл от CodeMirror. На нашей странице помещаем textarea c id="code", к которому и будет привязано отображение нашего "раскрашенного" исходника.
Две JavaScript функции описанные ниже textarea отвечают за установку содержимого в окно редактора и получение результата. Вынимаем результат мы спустя 300 ms после изменения содержимого редактора (WebView склонно тормозить), а устанавливаем, само собой, функцией setContent.
Отображаем локальную web-страницу с подгрузкой скриптов из asset-ов
public static String FILE_NAME = "filename"; private File mEdited; private EditText mNewFileName; private WebView wv; private String mRezContent; private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); progressDialog = new ProgressDialog(this); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMessage(getResources().getString(R.string.msg_loading)); progressDialog.setCancelable(false); mEdited = new File(getIntent().getExtras().getString(FILE_NAME)); mRezContent = getFileContent(); wv = new WebView(this); wv.getSettings().setSupportZoom(true); wv.getSettings().setJavaScriptEnabled(true); wv.addJavascriptInterface(new JavascriptInterface(), "Android"); wv.setWebChromeClient(new FileWebClient()); wv.loadUrl("file:///android_asset/editor.html"); wv.requestFocusFromTouch(); LinearLayout ll = new LinearLayout(this); ll.setOrientation(LinearLayout.VERTICAL); ll.addView(wv, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); setContentView(ll); progressDialog.setProgress(0); progressDialog.show(); }
Тут, во-первых, учитываем, что любая загрузка в WebView (даже локального файла) занимает время. Поэтому отображаем ProgressDialog. Во-вторых hml-страницу редактора (и все нужные библиотеки) укладываем в каталог assets в корне нашего проекта и загружаем его как file:///android_asset/имя файла.
Передаём данные в WebView и получаем их обратно
Имя файла, который будем редактировать передаётся в наше Activity через intent и получается как getIntent().getExtras().getString("key"). Контент файла получаем так:
private String getFileContent() { StringBuilder sb = new StringBuilder(); try { BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(mEdited))); String tmp = null; do { tmp = br.readLine(); if (tmp != null) sb.append(tmp).append("\n"); } while (tmp != null); } catch (Exception e) { e.printStackTrace(); } return sb.toString(); }
...а для того, чтобы уложить его в нашу страничку-редактор нужно дождаться полной её загрузки. Для этого мы при создании WebView указали для него клиента:
wv.setWebChromeClient(new FileWebClient());
который будет обновлять ProgressDialog по мере загрузки редактора, а при окончании загрузки укладывать в редактор тест редактируемого файла и останавливать ProgressDialog:
private class FileWebClient extends WebChromeClient { @Override public void onProgressChanged(WebView view, int newProgress) { if (newProgress<100) { progressDialog.setProgress(newProgress); } else { progressDialog.dismiss(); wv.loadUrl("javascript:setContent('" + UrlEncoder.encode(mRezContent) + "');"); } } }
Как видно, тут мы взаимодействуем со страницей, загруженной в WebView так же как это делают букмарклеты: "загружаем" в неё JavaScript-вызов по протоколу "javascript:". Получать данные из страницы мы будем иначе.
При создании WebView мы прописали ему
wv.addJavascriptInterface(new JavascriptInterface(), "Android");
Это значит, что из JavaScript внутри страницы-редактора будет доступен объект "Android", с методами, котрые мы реализуем в ещё одном inner-классе внутри нашего Activity:
private class JavascriptInterface { @SuppressWarnings("unused") public void contOut(String html) { mRezContent = html; } }
В результате (довольно-таки тяжело отлаживаемого) взаимодействия JavaScript-функции внутри WebView и Java-кода снаружи при каждом изменении контента в редакторе, в глобальной переменной mRezContent отобразится актуальный контент. Осталось его сохранить в файл. При этом клиент должен будет воспользоваться аппаратной кнопкой меню и выбрать опцию "сохранить" (файл будет переписан) или "сохранить как", клиент получит диалог, где сможет указать новое имя файла. Вот код требуемых для этого методов:
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.editor_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.save: saveToFile(false); return true; case R.id.save_as: saveToFile(true); return true; default: return super.onOptionsItemSelected(item); } } private void saveToFile(boolean needNewName) { if (needNewName) { mNewFileName = new EditText(this); mNewFileName.setText(mEdited.getName()); mNewFileName.setSingleLine(); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.msg_newfilename).setCancelable(false).setView(mNewFileName).setPositiveButton(R.string.btn_saveas, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String newFname = mNewFileName.getText().toString(); mEdited = new File(mEdited.getParent() + "/" + newFname); saveToFile(false); } }).setNegativeButton(R.string.btn_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); builder.create().show(); } else { BufferedWriter out = null; try { out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(mEdited))); out.write(mRezContent); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); } finally { try { if (out != null) { out.flush(); out.close(); } } catch (IOException ee) { } } } }
Первый метод строит меню из xml-ресурса, описанного в res/menu/editor-menu.xml:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/save" android:icon="@android:drawable/ic_menu_save" android:title="@string/btn_save"/> <item android:id="@+id/save_as" android:icon="@android:drawable/ic_menu_set_as" android:title="@string/btn_saveas"/> </menu>
Второй обрабатывает нажатие на элементы меню, третий - собственно сохраняет данные в файл, спрашивая имя файла, если это требуется.
Как теперь становится ясно, базовый функционал android-приложения для редактирования файлов с подсветкой исходного кода можно реализовать в одном Activity в 160 строк кода, используя кроме этого только пару функций на JavaScript и одну замечательную открытую библиотеку. Просто, не правда ли?
UrlEncoder у вас свой? Потому что URLEncoder из Java.net все пробелы меняет на "+". Если возможно, подскажите, пожалуйста как этого избежать?
ОтветитьУдалить