Java tutorial
/* * Copyright 2016 Igor Talankin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.italankin.dictionary.ui.main; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.support.design.widget.Snackbar; import android.support.v4.app.ShareCompat; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewTreeObserver; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.italankin.dictionary.App; import com.italankin.dictionary.R; import com.italankin.dictionary.dto.Result; import com.italankin.dictionary.dto.TranslationEx; import com.italankin.dictionary.ui.translation.TranslationActivity; import java.util.List; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import butterknife.Bind; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.OnFocusChange; import butterknife.OnLongClick; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; public class MainActivity extends AppCompatActivity { private static final int SWITCH_ANIM_DURATION = 450; private static final int SWAP_LANGS_ANIM_DURATION = 300; private static final long PROGRESS_ANIM_DURATION = 500; private static final int TOOLBAR_ANIM_IN_DURATION = 700; private static final float INPUT_SCROLL_PARALLAX_FACTOR = 1.5f; private static final int REQUEST_CODE_SHARE = 17; @Inject MainPresenter _presenter; //region Views @Bind(R.id.toolbar) Toolbar toolbar; /** * EditText for text input */ @Bind(R.id.edit_input) EditText mInput; @Bind(R.id.input_card) View mInputLayout; /** * TextView in toolbar indicating current destination language */ @Bind(R.id.text_to) TextView mTextDest; /** * TextView in toolbar indicating current source language */ @Bind(R.id.text_from) TextView mTextSource; /** * Arrow in the toolbar for switching languages */ @Bind(R.id.swap_langs) View mSwapLangs; @Bind(R.id.lookup) View mLookup; @Bind(R.id.text_transcription) TextView mTranscription; @Bind(R.id.toolbar_inner_layout) View mToolbarInnerLayout; @Bind(R.id.recycler_view) RecyclerView mRecyclerView; @Bind(R.id.progress_bar) ProgressBar mProgressBar; //endregion /** * Adapter used to display source languages */ private LanguageAdapter mLangsSourceAdapter; /** * Adapter used to display destination languages */ private LanguageAdapter mLangsDestAdapter; /** * Used to implement debouce mechanism via publishing lookup requests to this object */ private PublishSubject<String> mLookupEvents = PublishSubject.create(); /** * Subscription for {@link #mLookupEvents}. Performs {@link #startLookup(String)} upon receiving * items. */ private Subscription mLookupSub; private InputMethodManager mInputManager; private ClipboardManager mClipboardManager; private TranslationAdapter mRecyclerViewAdapter; /////////////////////////////////////////////////////////////////////////// // Activity callbacks /////////////////////////////////////////////////////////////////////////// @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); App.injector().inject(this); setContentView(R.layout.activity_main); ButterKnife.bind(this); mInputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); mClipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); _presenter.attach(this); setSupportActionBar(toolbar); setupInputLayout(); setupRecyclerView(); setControlsState(false); _presenter.loadLanguages(); } private void setupRecyclerView() { mRecyclerView.setHasFixedSize(true); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); Resources res = getResources(); int left = res.getDimensionPixelSize(R.dimen.list_item_margin_left); int top = res.getDimensionPixelSize(R.dimen.list_item_margin_top); int right = res.getDimensionPixelSize(R.dimen.list_item_margin_right); int bottom = res.getDimensionPixelSize(R.dimen.list_item_margin_bottom); mRecyclerView.addItemDecoration(new SimpleItemDecoration(left, top, right, bottom)); // allow input layout to be scrolled along with recycler view mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { updateInputLayoutPosition(); } }); top = getResources().getDimensionPixelSize(R.dimen.list_top_offset); int inputHeight = getResources().getDimensionPixelSize(R.dimen.input_panel_height); mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(), inputHeight + top, mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingBottom()); // on click listener mRecyclerViewAdapter = new TranslationAdapter(this); mRecyclerViewAdapter.setListener(new TranslationAdapter.OnAdapterItemClickListener() { @Override public void onItemClick(int position) { TranslationEx item = _presenter.getLastResult().translations.get(position); startActivity(TranslationActivity.getStartIntent(getApplicationContext(), item)); } @Override public void onItemMenuClick(int position, int menuItemId) { ClipData clip = null; TranslationEx item = _presenter.getLastResult().translations.get(position); switch (menuItemId) { case R.id.action_lookup_word: lookupNext(item.text); break; case R.id.action_copy_mean: clip = ClipData.newPlainText("means", item.means); break; case R.id.action_copy_synonyms: clip = ClipData.newPlainText("synonyms", item.synonyms); break; case R.id.action_copy_translation: clip = ClipData.newPlainText("translation", item.text); break; } if (clip != null) { mClipboardManager.setPrimaryClip(clip); Toast.makeText(MainActivity.this, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } } }); mRecyclerView.setAdapter(mRecyclerViewAdapter); } private void setupInputLayout() { mInputLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mInput.requestFocus(); mInputManager.showSoftInput(mInput, 0); } }); ViewTreeObserver vto = mInputLayout.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (Build.VERSION.SDK_INT >= 16) { ViewTreeObserver vto = mInputLayout.getViewTreeObserver(); vto.removeOnGlobalLayoutListener(this); } updateInputLayoutPosition(); } }); mInput.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) { lookupNext(mInput.getText().toString()); return true; } return false; } }); } @Override protected void onResume() { super.onResume(); // ensure we reattach presenter in case there're multiple activities being launched _presenter.attach(this); mLookupSub = mLookupEvents.filter(new Func1<String, Boolean>() { @Override public Boolean call(String s) { return !TextUtils.isEmpty(s); } }).debounce(600, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()).subscribe(new Action1<String>() { @Override public void call(String s) { startLookup(s); } }); } @Override protected void onPause() { super.onPause(); if (mLookupSub != null && !mLookupSub.isUnsubscribed()) { mLookupSub.unsubscribe(); mLookupSub = null; } if (isFinishing()) { _presenter.saveLanguages(); _presenter.detach(this); } } @Override protected void onNewIntent(Intent intent) { handleIntent(intent); } private boolean handleIntent(Intent intent) { if (intent.getType() != null) { String type = intent.getType(); String text = intent.getStringExtra(Intent.EXTRA_TEXT); text = text.substring(0, Math.min(text.length(), 80)); if (type != null && !TextUtils.isEmpty(text) && "text/plain".equals(type)) { intent.setType(null); mInput.setText(text); mInput.selectAll(); startLookup(text); } return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_SHARE && resultCode == RESULT_OK && _presenter.closeOnShare()) { finish(); } } @Override public void onBackPressed() { if (_presenter.backFocusSearchField()) { if (mInput.hasFocus()) { super.onBackPressed(); } else { mInput.requestFocus(); mInputManager.showSoftInput(mInput, 0); } } else { super.onBackPressed(); } } /////////////////////////////////////////////////////////////////////////// // ButterKnife /////////////////////////////////////////////////////////////////////////// @OnClick(R.id.lookup) void onLookupClick() { lookupNext(mInput.getText().toString()); } @OnFocusChange(R.id.edit_input) void onEditInputFocusChange(boolean hasFocus) { if (hasFocus) { mRecyclerView.smoothScrollToPosition(0); } } @OnLongClick(R.id.text_transcription) boolean onTranscriptionLongClick(View v) { ClipData clip = ClipData.newPlainText("transcription", mTranscription.getText().toString()); mClipboardManager.setPrimaryClip(clip); Toast.makeText(MainActivity.this, R.string.msg_transcription_copied, Toast.LENGTH_SHORT).show(); return true; } @OnClick(R.id.text_to) void onDestLangClick() { showDestLanguageDialog(); } @OnClick(R.id.text_from) void onSourceLangClick() { showSourceLanguageDialog(); } @OnClick(R.id.swap_langs) void onSwapLangsClick() { swapLanguages(); } @OnLongClick(R.id.swap_langs) boolean onSwapLangsLongClick() { Toast.makeText(MainActivity.this, R.string.toast_swap_langs, Toast.LENGTH_SHORT).show(); return true; } /////////////////////////////////////////////////////////////////////////// // Options menu /////////////////////////////////////////////////////////////////////////// @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_share: Result result = _presenter.getLastResult(); if (result != null && !result.isEmpty()) { Intent intent = ShareCompat.IntentBuilder.from(this).setType("text/plain") .setText(result.toString()).setSubject(result.text).createChooserIntent(); startActivityForResult(intent, REQUEST_CODE_SHARE); } else { Toast.makeText(MainActivity.this, R.string.error_share, Toast.LENGTH_SHORT).show(); } return true; case R.id.action_history: showHistoryDialog(); return true; case R.id.action_settings: Intent intent = new Intent("com.italankin.dictionary.SETTINGS"); startActivity(intent); return true; } return false; } /////////////////////////////////////////////////////////////////////////// // Views update /////////////////////////////////////////////////////////////////////////// private void updateTextViews() { String source = _presenter.getSourceLanguage().getName(); mTextSource.setText(source); String dest = _presenter.getDestLanguage().getName(); mTextDest.setText(dest); } /** * Sync scroll state of the top layout. */ private void updateInputLayoutPosition() { float max = mInputLayout.getHeight() * INPUT_SCROLL_PARALLAX_FACTOR + mInputLayout.getHeight() / 10f; // additional space float value = max; float abs = Math.abs(mRecyclerView.computeVerticalScrollOffset() / INPUT_SCROLL_PARALLAX_FACTOR); if (abs < max) { value = abs; } mInputLayout.setTranslationY(-value); } private void setControlsState(boolean enabled) { mInput.setEnabled(enabled); mLookup.setEnabled(enabled); mRecyclerView.setEnabled(enabled); } /////////////////////////////////////////////////////////////////////////// // Dialogs /////////////////////////////////////////////////////////////////////////// /** * Show dialog for choosing source language. */ public void showSourceLanguageDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.source); if (mLangsSourceAdapter == null) { mLangsSourceAdapter = new LanguageAdapter(this, _presenter.getLanguagesList()); } _presenter.sortLanguagesList(); mLangsSourceAdapter.notifyDataSetChanged(); builder.setAdapter(mLangsSourceAdapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { _presenter.setSourceLanguage(which); updateTextViews(); } }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); } /** * Show dialog for choosing destination language. */ public void showDestLanguageDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.destination); if (mLangsDestAdapter == null) { mLangsDestAdapter = new LanguageAdapter(this, _presenter.getLanguagesList()); } _presenter.sortLanguagesList(); mLangsDestAdapter.notifyDataSetChanged(); builder.setAdapter(mLangsDestAdapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { _presenter.setDestLanguage(which); updateTextViews(); } }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); } private void showHistoryDialog() { final String[] values = _presenter.getHistory(); if (values == null || values.length == 0) { Toast.makeText(this, R.string.msg_history_empty, Toast.LENGTH_SHORT).show(); return; } AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_history); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, values); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { _presenter.loadHistory(which); } }); builder.show(); } /////////////////////////////////////////////////////////////////////////// // Lookup /////////////////////////////////////////////////////////////////////////// /** * Emit a new lookup request. * * @param s text to lookup */ private void lookupNext(String s) { mLookupEvents.onNext(s); } /** * Start lookup process, sending actual query. * * @param text text to lookup */ private void startLookup(String text) { text = text.replaceAll("[^\\p{L}\\w -]", " ").trim(); mInput.setText(text); if (text.length() > 0) { _presenter.lookup(text); mInputManager.hideSoftInputFromWindow(mInput.getWindowToken(), 0); mInput.clearFocus(); showProgressBar(); } } /** * Called from {@link MainPresenter}, when languages were fetched from cache/net. */ public void onLanguagesResult() { mToolbarInnerLayout.setVisibility(View.VISIBLE); mToolbarInnerLayout.setTranslationY(-mToolbarInnerLayout.getHeight()); mToolbarInnerLayout.setAlpha(0); mToolbarInnerLayout.animate().alpha(1).translationY(0).setDuration(TOOLBAR_ANIM_IN_DURATION) .setInterpolator(new DecelerateInterpolator(2.5f)).start(); setControlsState(true); updateTextViews(); // if we are not coming from share intent if (!handleIntent(getIntent())) { Result lastResult = _presenter.getLastResult(); if (lastResult == null) { mInput.requestFocus(); } else { onLookupResult(lastResult); } } } private void swapLanguages() { if (!_presenter.swapLanguages()) { return; } lookupNext(mInput.getText().toString()); // anim stuff float rotation = 180f; if (mSwapLangs.getRotation() > 0) { rotation *= -1; } mSwapLangs.setRotation(0); mSwapLangs.animate().rotationBy(rotation).setDuration(SWAP_LANGS_ANIM_DURATION).start(); SwitchAnimation anim = new SwitchAnimation(mTextSource, mTextSource.getHeight(), 0f, SWITCH_ANIM_DURATION, new SwitchAnimation.OnSwitchListener() { @Override public void onSwitch() { updateTextViews(); } }); anim.start(); anim = new SwitchAnimation(mTextDest, -mTextDest.getHeight(), 0, SWITCH_ANIM_DURATION, null); anim.start(); } /** * Called from {@link MainPresenter} when received lookup result from server * * @param result result object returned from server */ public void onLookupResult(Result result) { List<TranslationEx> translations = result.translations; mInput.setText(result.text); if (!TextUtils.isEmpty(result.transcription)) { mTranscription.setText(String.format("[%s]", result.transcription)); } else { mTranscription.setText(""); } mRecyclerViewAdapter.setData(translations); hideProgressBar(); updateInputLayoutPosition(); } /////////////////////////////////////////////////////////////////////////// // Errors /////////////////////////////////////////////////////////////////////////// /** * Called when error occured. Typically it's related to server or connection problems/wrong * request. * * @param messaage text to show */ public void onError(String messaage) { if (messaage == null) { messaage = getString(R.string.error); } Snackbar snackbar = Snackbar.make(mInput, messaage, Snackbar.LENGTH_LONG); snackbar.setAction(android.R.string.ok, new View.OnClickListener() { @Override public void onClick(View v) { // empty callback for showing OK button } }); snackbar.show(); hideProgressBar(); } /** * Show error when no results were received. */ public void onNoResults() { onError(getString(R.string.error_no_results)); } /** * If error was occured while fetching languages. */ public void onLanguagesError() { setControlsState(false); Snackbar snackbar = Snackbar.make(mInput, R.string.error_langs, Snackbar.LENGTH_INDEFINITE); snackbar.setAction(R.string.retry, new View.OnClickListener() { @Override public void onClick(View v) { _presenter.loadLanguages(); } }); snackbar.show(); } /////////////////////////////////////////////////////////////////////////// // Utility /////////////////////////////////////////////////////////////////////////// /** * Show the progress bar. */ private void showProgressBar() { if (mProgressBar.getVisibility() == View.INVISIBLE) { mProgressBar.setAlpha(0); mProgressBar.setVisibility(View.VISIBLE); } mProgressBar.animate().alpha(1).setListener(null).setDuration(PROGRESS_ANIM_DURATION).start(); } /** * Hide the progress bar. */ private void hideProgressBar() { if (mProgressBar.getVisibility() == View.VISIBLE) { mProgressBar.animate().alpha(0).setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mProgressBar.setVisibility(View.INVISIBLE); } }).setDuration(PROGRESS_ANIM_DURATION).start(); } } }