package cn.zhaiyifan.rememberedittext; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.inputmethod.InputMethodManager; import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupWindow; import android.widget.TextView; import java.util.LinkedList; import java.util.List; public class RememberEditText extends EditText { private static final String PREFERENCE_KEY = "RememberEditText"; private int mRememberCount = 1; private String mRememberId; private boolean mAutoFill = true; private Drawable mDeleteDrawable; private Drawable mDropDownDrawable; private Drawable mDropUpDrawable; private PopupWindow pop; private Rect mDropDownIconRect = new Rect(); private Rect mDeleteIconRect = new Rect(); private int mIconStatus = ICON_ABSENT; private int mIconMargin = 0; private static final int ICON_SHOW_DROP_DOWN = 1; private static final int ICON_SHOW_DROP_UP = 2; private static final int ICON_ABSENT = 3; private static final int DEFAULT_REMEMBER_COUNT = 3; private static final int DEFAULT_ICON_MARGIN_IN_DP = 15; private static PersistedMap mCacheMap; protected List<String> mCacheDataList; public RememberEditText(Context context, AttributeSet attrs) { super(context, attrs); initAttrs(attrs); initCacheMap(context); initData(); } public RememberEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initAttrs(attrs); initCacheMap(context); initData(); } private void initAttrs(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RememberEditText); try { mDeleteDrawable = getResources().getDrawable(a.getResourceId( R.styleable.RememberEditText_deleteIcon, R.drawable.ic_delete)); if (mDeleteDrawable != null) { mDeleteDrawable.setBounds(0, 0, mDeleteDrawable.getIntrinsicWidth(), mDeleteDrawable.getIntrinsicHeight()); } mDropDownDrawable = getResources().getDrawable(a.getResourceId( R.styleable.RememberEditText_dropDownIcon, R.drawable.ic_drop_down)); if (mDropDownDrawable != null) { mDropDownDrawable.setBounds(0, 0, mDropDownDrawable.getIntrinsicWidth(), mDropDownDrawable.getIntrinsicHeight()); } mDropUpDrawable = getResources().getDrawable(a.getResourceId( R.styleable.RememberEditText_dropUpIcon, R.drawable.ic_drop_up)); if (mDropUpDrawable != null) { mDropUpDrawable.setBounds(0, 0, mDropUpDrawable.getIntrinsicWidth(), mDropUpDrawable.getIntrinsicHeight()); } mRememberCount = a.getInt(R.styleable.RememberEditText_rememberCount, DEFAULT_REMEMBER_COUNT); mRememberId = a.getString(R.styleable.RememberEditText_rememberId); // if not set rememberId, use view id if (null == mRememberId) { mRememberId = String.valueOf(getId()); } mAutoFill = a.getBoolean(R.styleable.RememberEditText_autoFill, true); } finally { a.recycle(); } } /** * restore last recent input */ private void initData() { DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); float density = metrics.density; mIconMargin = Math.round(DEFAULT_ICON_MARGIN_IN_DP * density); mCacheDataList = new LinkedList<>(); String lastCache = mCacheMap.get(mRememberId); if (mAutoFill) { setText(lastCache); } if (lastCache != null) { mCacheDataList.add(0, lastCache); // Retrieve all history data for (int i = 1; i < mRememberCount; ++i) { String data = mCacheMap.get(mRememberId + i); if (data != null) { mCacheDataList.add(i, data); } } onCacheDataChanged(); } } /** * Called when cached data changed(init or deleted). */ private void onCacheDataChanged() { if (mCacheDataList.size() >= 1) { mIconStatus = ICON_SHOW_DROP_DOWN; } else { mIconStatus = ICON_ABSENT; } } /** * Do cache flush jobs when view detached. */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); // if have text, save it String text = getText().toString(); // save newest input to default key if (text.length() > 0) { mCacheMap.put(mRememberId, text); if (mCacheDataList.isEmpty() || !text.equals(mCacheDataList.get(0))) { mCacheDataList.add(0, text); } } // flush history for (int i = 1; i < mCacheDataList.size(); ++i) { mCacheMap.put(mRememberId + i, mCacheDataList.get(i)); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); final int compoundPaddingTop = getCompoundPaddingTop(); final int compoundPaddingBottom = getCompoundPaddingBottom(); int vspace = getBottom() - getTop() - compoundPaddingBottom - compoundPaddingTop; int drawableHeight = mDeleteDrawable.getIntrinsicHeight(); int dropDownWidth = mDropDownDrawable.getIntrinsicWidth(); int deleteWidth = mDeleteDrawable.getIntrinsicWidth(); int offsetX = getMeasuredWidth() - getCompoundPaddingRight() - dropDownWidth - mIconMargin - deleteWidth; int offsetY = getCompoundPaddingTop() + getScrollY() + (vspace - drawableHeight) / 2; canvas.translate(offsetX, offsetY); if (getText() != null && getText().length() != 0) { mDeleteDrawable.draw(canvas); } canvas.translate(deleteWidth + mIconMargin, 0); switch (mIconStatus) { case ICON_SHOW_DROP_DOWN: mDropDownDrawable.draw(canvas); break; case ICON_SHOW_DROP_UP: mDropUpDrawable.draw(canvas); break; case ICON_ABSENT: break; } mDeleteIconRect.set(offsetX, offsetY, offsetX + deleteWidth, offsetY + drawableHeight); mDropDownIconRect.set(offsetX + deleteWidth + mIconMargin, offsetY, offsetX + deleteWidth + mIconMargin + dropDownWidth, offsetY + drawableHeight); canvas.restore(); } /** * Override onTouchEvent, check icon click event. See * {@link android.text.method.ArrowKeyMovementMethod} for EditText touch event handle. */ @Override public boolean onTouchEvent(MotionEvent event) { // If touch icon fields, intercept event to prevent further handle. boolean handled = false; final int action = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); if (action == MotionEvent.ACTION_UP) { if (isInRect(mDropDownIconRect, x, y)) { handled = true; if (pop == null) { showPopupWindow(); } else { disMissOrUpdatePopupWindow(); } } else if (isInRect(mDeleteIconRect, x, y)) { setText(""); handled = true; } } return handled || super.onTouchEvent(event); } /** * Different from {@link Rect}'s contains method, it is more tolerant. */ private boolean isInRect(Rect rect, int x, int y) { return x > rect.left - mIconMargin / 2 && x < rect.right + mIconMargin / 2; } private static void initCacheMap(Context context) { if (mCacheMap == null) { mCacheMap = new PersistedMap(context, PREFERENCE_KEY); } } /** * Clear all cache managed by RememberEditText. */ public static void clearCache(Context context) { initCacheMap(context); mCacheMap.clear(); } // ===================== popup window ===================== private LinearLayout mCacheListWrapperLinearLayout; private ListView mCacheListView; private boolean mKeyboardShown; private RememberListAdapter mListAdapter; private int mPopupWindowHeight; public void showPopupWindow() { // no cache data, return directly if (mCacheDataList == null || mCacheDataList.size() == 0) { return; } if (mListAdapter == null) { mListAdapter = new RememberListAdapter(); } // if soft keyboard is shown, hide it before show popup if (mKeyboardShown) { // hide keyboard InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getWindowToken(), 0); postDelayed(new Runnable() { @Override public void run() { willShowPopupWindow(); } }, 550); } else { willShowPopupWindow(); } } private void willShowPopupWindow() { int userCount = mListAdapter.getCount(); if (userCount > mRememberCount) { userCount = mRememberCount; } int itemHeight = getResources().getDimensionPixelSize(R.dimen.remember_item_height); mPopupWindowHeight = itemHeight * userCount + (userCount - 1); if (pop == null) { mCacheListView = new ListView(getContext()); mCacheListView.setCacheColorHint(0); mCacheListView.setScrollingCacheEnabled(false); mCacheListView.setFadingEdgeLength(0); mCacheListView.setDivider(new ColorDrawable(Color.GRAY)); mCacheListView.setDividerHeight(1); LayoutParams params = new LayoutParams(getWidth(), mPopupWindowHeight); mCacheListView.setLayoutParams(params); mCacheListWrapperLinearLayout = new LinearLayout(getContext()); mCacheListWrapperLinearLayout.setLayoutParams(params); mCacheListWrapperLinearLayout.addView(mCacheListView); pop = new SafePopupWindow(mCacheListWrapperLinearLayout, getWidth(), LayoutParams.WRAP_CONTENT); mCacheListView.setAdapter(mListAdapter); } pop.setAnimationStyle(R.style.RememberEditTextPopupWindowAnim); pop.showAsDropDown(this); mListAdapter.notifyDataSetChanged(); mIconStatus = ICON_SHOW_DROP_UP; } public void disMissOrUpdatePopupWindow() { if (pop != null) { mIconStatus = ICON_SHOW_DROP_DOWN; if (pop.isShowing()) { pop.dismiss(); pop = null; } } } /** * Update EditText text after selected * @param position selected position in cached list * @return whether position is legal and event is handled */ private boolean selectItem(int position) { disMissOrUpdatePopupWindow(); if (position >= 0 && mCacheDataList.size() > position) { setText(mCacheDataList.get(position)); return true; } return false; } /** * RememberEditText's dropdown list adapter. */ class RememberListAdapter extends BaseAdapter { LayoutInflater mInflater; public RememberListAdapter() { mInflater = LayoutInflater.from(getContext()); } @Override public int getCount() { if (mCacheDataList != null) { return mCacheDataList.size() > mRememberCount ? mRememberCount : mCacheDataList.size(); } return 0; } @Override public Object getItem(int position) { if (mCacheDataList != null) { return mCacheDataList.get(position); } return null; } @Override public long getItemId(int position) { return position; } @Override public View getView(final int position, View convertView, ViewGroup parent) { Holder holder; if (convertView == null) { convertView = mInflater.inflate(R.layout.item_remember_cache_list, null); holder = new Holder(); holder.wrapper = convertView.findViewById(R.id.popupWrapper); holder.view = (TextView) convertView.findViewById(R.id.popupContent); holder.button = (ImageView) convertView.findViewById(R.id.popupDelete); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } final String cacheData = (String) getItem(position); if (holder != null && cacheData != null) { convertView.setId(position); holder.setId(position); holder.view.setText(cacheData); convertView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { selectItem(position); } }); holder.button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mCacheDataList != null && mCacheDataList.size() > 0 && position < mCacheDataList.size()) { mCacheDataList.remove(position); } if (mCacheDataList == null || (mCacheDataList != null && mCacheDataList.size() < 1)) { onCacheDataChanged(); } if (getText().toString().trim().equalsIgnoreCase(cacheData)) { setText(""); } mListAdapter.notifyDataSetChanged(); if (mCacheDataList.size() == 0) { disMissOrUpdatePopupWindow(); return; } if (mCacheDataList.size() < mRememberCount) { int itemHeight = getResources().getDimensionPixelSize(R.dimen.remember_item_height); int currHeight = itemHeight * mCacheDataList.size() + (mCacheDataList.size() - 1); mCacheListView.getLayoutParams().height = currHeight; mCacheListWrapperLinearLayout.getLayoutParams().height = currHeight; mCacheListView.requestLayout(); mPopupWindowHeight = currHeight; } } }); } return convertView; } class Holder { View wrapper; TextView view; ImageView button; void setId(int position) { view.setId(position); button.setId(position); wrapper.setId(position); } } } }