package com.datdo.mobilib.util; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.text.Html; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.ScrollView; import android.widget.TextView; import com.datdo.mobilib.base.MblDecorView; import com.datdo.mobilib.event.MblCommonEvents; import com.datdo.mobilib.event.MblEventCenter; import com.datdo.mobilib.event.MblEventListener; import com.datdo.mobilib.event.MblStrongEventListener; import junit.framework.Assert; import java.util.ArrayList; import java.util.List; import com.datdo.mobilib.util.MblLinkMovementMethod.*; /** * <pre> * Utility class containing methods designed to deal with common problems/features when processing views * </pre> */ public class MblViewUtil { private static final String TAG = MblUtils.getTag(MblViewUtil.class); /** * <pre> * Traverse view and all its sub-view. * </pre> * @param view view at top level * @param delegate {@link MblInterateViewDelegate} */ public static void iterateView(View view, MblInterateViewDelegate delegate) { if (view == null) { return; } if (delegate != null) { delegate.process(view); } if (view instanceof ViewGroup) { ViewGroup vg = (ViewGroup) view; int size = vg.getChildCount(); for (int i = 0; i < size; i++) { iterateView(vg.getChildAt(i), delegate); } } } /** * <pre> * Delegate interface to customize the processing for view and sub-view traversed in {@link #iterateView(View, MblInterateViewDelegate)} * </pre> */ public static interface MblInterateViewDelegate { public void process(View view); } /** * <pre> * We sometimes encounter a problem like this: ScrollView contains its content View, and we want the content View always keep its height equal to original ScrollView 's height even when ScrollView 's height is changed (for example: keyboard ON/OFF) * Use this method to fix content View 's height always equal to original ScrollView 's height. * A popular case where this method should be used is when you need to set background image for activity, but you don't want background image to shrink when keyboard is ON. * In that case, just put an ImageView inside a ScrollView and call <code>fixScrollViewContentHeight(scrollView)</code> * </pre> * @param sv */ public static void fixScrollViewContentHeight(final ScrollView sv) { if (sv == null || sv.getChildCount() == 0) { return; } final int WRAPPING_VIEW_ID = 1427684961; if (sv.getHeight() == 0) { sv.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { MblUtils.removeOnGlobalLayoutListener(sv, this); fixScrollViewContentHeight(sv); } }); return; } if (sv.getChildAt(0).getId() != WRAPPING_VIEW_ID) { // get content view, then remove it from ScrollView View contentView = sv.getChildAt(0); if (contentView == null) { return; } sv.removeAllViews(); // create a wrapping view FrameLayout wrappingView = new FrameLayout(sv.getContext()); wrappingView.setLayoutParams(new ScrollView.LayoutParams( ScrollView.LayoutParams.MATCH_PARENT, ScrollView.LayoutParams.WRAP_CONTENT)); wrappingView.setId(WRAPPING_VIEW_ID); sv.addView(wrappingView); // set content view 's height and attach it to wrapping view contentView.setLayoutParams(new FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, sv.getHeight())); wrappingView.addView(contentView); } else{ FrameLayout wrappingView = (FrameLayout) sv.getChildAt(0); View contentView = wrappingView.getChildAt(0); FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) contentView.getLayoutParams(); lp.width = FrameLayout.LayoutParams.MATCH_PARENT; lp.height = sv.getHeight(); contentView.setLayoutParams(lp); } } /** * <pre> * Set background image for a screen, and ensure that it won't shrink even when decor view size is changed (keyboard ON/OFF, orientation change, etc) * </pre> * @param decorView top-level view of screen * @param portraitBitmap bitmap data for portrait orientation * @param landscapeBitmap bitmap data for landscape orientation */ public static ImageView setBackgroundNoShrinking( ViewGroup decorView, final Bitmap portraitBitmap, final Bitmap landscapeBitmap) { Assert.assertNotNull(decorView); final ScrollView sv = new ScrollView(decorView.getContext()) { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent ev) { return false; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } }; sv.setLayoutParams(new MblDecorView.LayoutParams( MblDecorView.LayoutParams.MATCH_PARENT, MblDecorView.LayoutParams.MATCH_PARENT)); final EventListeningImageView iv = new EventListeningImageView(decorView.getContext()); iv.mEventListener = new MblEventListener() { @Override public void onEvent(Object sender, String name, Object... args) { if (MblUtils.isKeyboardOn()) { MblEventCenter.addListener(new MblStrongEventListener() { @Override public void onEvent(Object sender, String name, Object... args) { justifyHeight(); terminate(); } }, MblCommonEvents.KEYBOARD_HIDDEN); MblUtils.hideKeyboard(); } else { justifyHeight(); } } void justifyHeight() { MblUtils.getMainThreadHandler().post(new Runnable() { @Override public void run() { fixScrollViewContentHeight(sv); if (MblUtils.isPortraitDisplay()) { iv.setImageBitmap(portraitBitmap); } else { iv.setImageBitmap(landscapeBitmap); } } }); } }; MblEventCenter.addListener(iv.mEventListener, MblCommonEvents.ORIENTATION_CHANGED); iv.setLayoutParams(new ScrollView.LayoutParams( ScrollView.LayoutParams.MATCH_PARENT, ScrollView.LayoutParams.WRAP_CONTENT)); iv.setScaleType(ScaleType.CENTER_CROP); sv.addView(iv); decorView.addView(sv, 0); iv.mEventListener.onEvent(null, null); return iv; } private static class EventListeningImageView extends ImageView { MblEventListener mEventListener; public EventListeningImageView(Context context) { super(context); } } /** * <pre>Same as {@link #setBackgroundNoShrinking(ViewGroup, Bitmap, Bitmap)}</pre> */ public static ImageView setBackgroundNoShrinking(ViewGroup decorView, int portraitBgResId, int landscapeBgResId) { Drawable portraitDrawable = MblUtils.getCurrentContext().getResources().getDrawable(portraitBgResId); Bitmap portraitBitmap = null; if (portraitDrawable instanceof BitmapDrawable) { portraitBitmap = ((BitmapDrawable) portraitDrawable).getBitmap(); } Drawable landscapeDrawable = MblUtils.getCurrentContext().getResources().getDrawable(landscapeBgResId); Bitmap landscapeBitmap = null; if (landscapeDrawable instanceof BitmapDrawable) { landscapeBitmap = ((BitmapDrawable) landscapeDrawable).getBitmap(); } return setBackgroundNoShrinking(decorView, portraitBitmap, landscapeBitmap); } /** * <pre> * Android keyboard sometimes overlaps the {@link EditText}, especially when it supports text prediction feature. * Also, {@link EditText} sometimes doesn't scroll long enough to be displayed when keyboard is ON, which makes it partially hidden. * This method makes EditText always scroll to best position so that user can see it clearly and fully. * </pre> * @param decoreView top-level view of screen * @param editTexts array of {@link EditText} to enable auto-scroll * @param parentScrollView parent {@link ScrollView} that contains {@link EditText} in <code>editTexts</code> */ public static void makeEditTextAutoScrollOnFocused( final View decoreView, final EditText[] editTexts, final ScrollView parentScrollView) { Assert.assertNotNull(decoreView); Assert.assertNotNull(editTexts); Assert.assertNotNull(parentScrollView); final Runnable action = new Runnable() { @Override public void run() { // if keyboard is not ON, we have nothing to do if (!MblUtils.isKeyboardOn()) { return; } // find the EditText being focused View focusedView = ((Activity)decoreView.getContext()).getCurrentFocus(); EditText focusedEditText = null; for (EditText et : editTexts) { if (et == focusedView) { focusedEditText = et; } } // force ScrollView scroll so that focused EditText is centered if (focusedEditText != null) { int[] editTextLocation = new int[2]; focusedEditText.getLocationOnScreen(editTextLocation); int[] decorViewLocation = new int[2]; decoreView.getLocationOnScreen(decorViewLocation); int editTextYPos = editTextLocation[1] - decorViewLocation[1]; final int dy = (editTextYPos + focusedEditText.getHeight() / 2) - decoreView.getHeight() / 2; parentScrollView.smoothScrollBy(0, dy); } } }; parentScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { action.run(); } }); for (EditText et : editTexts) { et.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { return; } action.run(); } }); }; } /** * <pre> * Same as {@link #makeEditTextAutoScrollOnFocused(View, EditText[], ScrollView)}. * <code>editTexts</code> is omitted. {@link EditText} objects are collected automatically by traversing view. * </pre> */ public static void makeEditTextAutoScrollOnFocused( final View decoreView, final ScrollView parentScrollView) { final List<EditText> editTexts = new ArrayList<EditText>(); iterateView(parentScrollView, new MblInterateViewDelegate() { @Override public void process(View view) { if (view instanceof EditText) { editTexts.add((EditText) view); } } }); makeEditTextAutoScrollOnFocused( decoreView, editTexts.toArray(new EditText[editTexts.size()]), parentScrollView); } /** * <pre>Extract {@link String} instance from {@link TextView}</pre> */ public static String extractText(TextView editText) { return MblUtils.trim(editText.getText().toString()); } private static MblInterateViewDelegate sGlobalViewProcessor; /** * Get global view processor set by {@link #setGlobalViewProcessor(MblInterateViewDelegate)} */ public static MblInterateViewDelegate getGlobalViewProcessor() { return sGlobalViewProcessor; } /** * <pre> * Customize the way to process each view in the activity 's layout. * This method is used to apply common processing to views like setting default font, default behaviours, default attributes, etc * </pre> */ public static void setGlobalViewProcessor(MblInterateViewDelegate globalViewProcessor) { sGlobalViewProcessor = globalViewProcessor; } /** * <pre> * Display a string that may contains links (email, web-url, phone) using {@link TextView}. Links are clickable. * </pre> * @param callback customize how to handle link-clicked and long-clicked * @param options options for {@link MblLinkRecognizer} */ public static void displayTextWithLinks(TextView textView, String content, MblLinkRecognizer.MblOptions options, final MblLinkMovementMethodCallback callback) { String html = MblLinkRecognizer.getLinkRecognizedHtmlText(content, options); textView.setText(Html.fromHtml(html)); textView.setMovementMethod(new MblLinkMovementMethod(new MblLinkMovementMethodCallback() { @Override public void onLinkClicked(final String link) { try { if (MblUtils.isEmail(link)) { MblUtils.sendEmail(null, new String[]{link}, null, null, null, null, null); } else if (MblUtils.isWebUrl(link)) { MblUtils.openWebUrl(link); } else if (MblUtils.isPhone(link)) { if (MblUtils.hasPhone()) { MblUtils.getCurrentContext().startActivity( new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + link))); } } } catch (Exception e) { Log.e(TAG, "Can not open link: " + link, e); } if (callback != null) { callback.onLinkClicked(link); } } @Override public void onLongClicked() { if (callback != null) { callback.onLongClicked(); } } })); } }