Last active
January 11, 2017 06:13
-
-
Save henrytao-me/20a5d411de69d983173dc3ce13862507 to your computer and use it in GitHub Desktop.
Ultimate holder for building custom in-app keyboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2017 "Henry Tao <[email protected]>" | |
* | |
* 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 me.henrytao.widget; | |
import android.app.Activity; | |
import android.app.FragmentManager; | |
import android.content.Context; | |
import android.content.SharedPreferences; | |
import android.graphics.Color; | |
import android.graphics.Point; | |
import android.graphics.Rect; | |
import android.graphics.drawable.ColorDrawable; | |
import android.os.Build; | |
import android.preference.PreferenceManager; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.RequiresApi; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.Gravity; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.view.WindowManager; | |
import android.view.inputmethod.InputMethodManager; | |
import android.widget.EditText; | |
import android.widget.FrameLayout; | |
import android.widget.PopupWindow; | |
import java.util.Locale; | |
/** | |
* Created by henrytao on 12/26/16. | |
*/ | |
public class KeyboardHolder extends FrameLayout { | |
private static final String KEY = "KEYBOARD_HOLDER"; | |
private static final String PREF_KEYBOARD_HEIGHT = "PREF_KEYBOARD_HEIGHT"; | |
public static boolean DEBUG = false; | |
private static int sKeyboardHeightInCache = 0; | |
private static SharedPreferences sPreferences; | |
public static int getKeyboardHeightInCache(Context context) { | |
sKeyboardHeightInCache = sKeyboardHeightInCache > 0 ? sKeyboardHeightInCache : getPreferences(context).getInt(PREF_KEYBOARD_HEIGHT, 0); | |
return sKeyboardHeightInCache; | |
} | |
@NonNull | |
private static SharedPreferences getPreferences(Context context) { | |
if (sPreferences == null) { | |
synchronized (KeyboardHolder.class) { | |
if (sPreferences == null) { | |
sPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); | |
} | |
} | |
} | |
return sPreferences; | |
} | |
private static void log(String format, Object... args) { | |
if (DEBUG) { | |
Log.d(KeyboardHolder.class.getSimpleName(), String.format(Locale.US, format, args)); | |
} | |
} | |
private static void setKeyboardHeightToCache(Context context, int keyboardHeight) { | |
if (getKeyboardHeightInCache(context) != keyboardHeight) { | |
sKeyboardHeightInCache = keyboardHeight; | |
getPreferences(context).edit().putInt(PREF_KEYBOARD_HEIGHT, keyboardHeight).apply(); | |
} | |
} | |
private Activity mActivity; | |
private boolean mBackStackEnabled = true; | |
private int mBackStackEntryCount = -1; | |
private boolean mCacheEnabled = true; | |
private Detector mDetector; | |
private int mKeyboardHeight; | |
private OnKeyboardLayoutChangedListener mOnKeyboardLayoutChangedListener; | |
private boolean mShouldInitKeyboardHeight; | |
private boolean mShouldKeepHolder; | |
private State mState = State.NONE; | |
public KeyboardHolder(Context context) { | |
super(context); | |
initialize(); | |
} | |
public KeyboardHolder(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
initialize(); | |
} | |
public KeyboardHolder(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
initialize(); | |
} | |
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) | |
public KeyboardHolder(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | |
super(context, attrs, defStyleAttr, defStyleRes); | |
initialize(); | |
} | |
@Override | |
protected void onAttachedToWindow() { | |
super.onAttachedToWindow(); | |
log("onAttachedToWindow"); | |
if (mDetector != null) { | |
mDetector.start(); | |
} | |
} | |
@Override | |
protected void onDetachedFromWindow() { | |
super.onDetachedFromWindow(); | |
log("onDetachedFromWindow"); | |
if (mDetector != null) { | |
mDetector.stop(); | |
} | |
} | |
public void hide() { | |
mShouldKeepHolder = false; | |
hideKeyboard(); | |
toggle(false); | |
removeBackStack(); | |
onHide(); | |
} | |
public boolean isBackStackEnabled() { | |
return mBackStackEnabled; | |
} | |
public void setBackStackEnabled(boolean backStackEnabled) { | |
mBackStackEnabled = backStackEnabled; | |
} | |
public boolean isCacheEnabled() { | |
return mCacheEnabled; | |
} | |
public void setCacheEnabled(boolean cacheEnabled) { | |
mCacheEnabled = cacheEnabled; | |
} | |
public boolean isShowing() { | |
return mState != State.NONE; | |
} | |
public void setOnKeyboardLayoutChangedListener(OnKeyboardLayoutChangedListener onKeyboardLayoutChangedListener) { | |
mOnKeyboardLayoutChangedListener = onKeyboardLayoutChangedListener; | |
} | |
public void setSupportActivity(Activity activity) { | |
log("setSupportActivity"); | |
mActivity = activity; | |
mDetector = new Detector(activity, this::onKeyboardLayoutChange); | |
mActivity.getFragmentManager().addOnBackStackChangedListener(() -> { | |
if (mBackStackEnabled && mActivity.getFragmentManager().getBackStackEntryCount() == mBackStackEntryCount) { | |
hide(); | |
} | |
}); | |
} | |
public void showHolder(@NonNull EditText editText) { | |
mShouldKeepHolder = true; | |
if (getKeyboardHeight() > 0) { | |
editText.requestFocus(); | |
toggle(true); | |
hideKeyboard(); | |
onHolderShowed(); | |
} else { | |
mShouldInitKeyboardHeight = true; | |
showKeyboard(editText); | |
} | |
} | |
public void showKeyboard(@NonNull EditText editText) { | |
editText.requestFocus(); | |
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | |
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); | |
} | |
void initialize() { | |
} | |
void onKeyboardLayoutChange(int height) { | |
setKeyboardHeight(height); | |
if (height > 0) { | |
toggle(true); | |
if (mShouldInitKeyboardHeight) { | |
mShouldInitKeyboardHeight = false; | |
hideKeyboard(); | |
onHolderShowed(); | |
} else { | |
onKeyboardShowed(); | |
} | |
} else { | |
if (mShouldKeepHolder) { | |
mShouldKeepHolder = false; | |
} else { | |
toggle(false); | |
onHide(); | |
} | |
} | |
} | |
private void addBackStack() { | |
if (mBackStackEnabled) { | |
FragmentManager fm = mActivity.getFragmentManager(); | |
int count = fm.getBackStackEntryCount(); | |
if (count > 0 && KEY.equals(fm.getBackStackEntryAt(count - 1).getName())) { | |
return; | |
} | |
mBackStackEntryCount = count; | |
fm.beginTransaction() | |
.addToBackStack(KEY) | |
.commit(); | |
} | |
} | |
private int getKeyboardHeight() { | |
mKeyboardHeight = mKeyboardHeight > 0 ? mKeyboardHeight : (mCacheEnabled ? getKeyboardHeightInCache(getContext()) : 0); | |
return mKeyboardHeight; | |
} | |
private void setKeyboardHeight(int keyboardHeight) { | |
mKeyboardHeight = keyboardHeight > 0 ? keyboardHeight : getKeyboardHeight(); | |
setKeyboardHeightToCache(getContext(), getKeyboardHeight()); | |
} | |
private void hideKeyboard() { | |
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); | |
imm.hideSoftInputFromWindow(getWindowToken(), 0); | |
} | |
private void notifyKeyboardLayoutChange() { | |
if (mOnKeyboardLayoutChangedListener != null) { | |
mOnKeyboardLayoutChangedListener.onKeyboardLayoutChanged(this, mState == State.NONE ? 0 : getKeyboardHeight(), mState); | |
} | |
} | |
private void onHide() { | |
mShouldKeepHolder = false; | |
removeBackStack(); | |
mState = State.NONE; | |
notifyKeyboardLayoutChange(); | |
} | |
private void onHolderShowed() { | |
addBackStack(); | |
mState = State.HOLDER; | |
notifyKeyboardLayoutChange(); | |
} | |
private void onKeyboardShowed() { | |
mShouldKeepHolder = false; | |
removeBackStack(); | |
mState = State.KEYBOARD; | |
notifyKeyboardLayoutChange(); | |
} | |
private void removeBackStack() { | |
if (mBackStackEnabled) { | |
mBackStackEntryCount = -1; | |
FragmentManager fm = mActivity.getFragmentManager(); | |
if (fm.getBackStackEntryCount() > 0) { | |
try { | |
fm.popBackStackImmediate(KEY, FragmentManager.POP_BACK_STACK_INCLUSIVE); | |
} catch (Exception ignore) { | |
} | |
} | |
} | |
} | |
private void toggle(boolean showed) { | |
int keyboardHeight = showed ? getKeyboardHeight() : 0; | |
ViewGroup.LayoutParams params = getLayoutParams(); | |
if (params.height != keyboardHeight) { | |
params.height = keyboardHeight; | |
setLayoutParams(params); | |
} | |
} | |
public enum State { | |
KEYBOARD, HOLDER, NONE | |
} | |
public interface OnKeyboardLayoutChangedListener { | |
void onKeyboardLayoutChanged(View view, int height, State state); | |
} | |
private static class Detector extends PopupWindow { | |
private final Activity mActivity; | |
private final OnKeyboardLayoutChangedListener mOnKeyboardLayoutChangedListener; | |
private final View mParentView; | |
Detector(Activity activity, OnKeyboardLayoutChangedListener onKeyboardLayoutChangedListener) { | |
mActivity = activity; | |
mOnKeyboardLayoutChangedListener = onKeyboardLayoutChangedListener; | |
mParentView = mActivity.findViewById(android.R.id.content); | |
FrameLayout view = new FrameLayout(activity.getApplicationContext()); | |
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); | |
setContentView(view); | |
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); | |
setInputMethodMode(INPUT_METHOD_NEEDED); | |
setWidth(0); | |
setHeight(ViewGroup.LayoutParams.MATCH_PARENT); | |
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(this::onGlobalLayoutChanged); | |
} | |
void start() { | |
log("detector start"); | |
if (!isShowing() && mParentView != null) { | |
setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); | |
showAtLocation(mParentView, Gravity.NO_GRAVITY, 0, 0); | |
} | |
} | |
void stop() { | |
log("detector stop"); | |
dismiss(); | |
} | |
private void onGlobalLayoutChanged() { | |
Point screenSize = new Point(); | |
mActivity.getWindowManager().getDefaultDisplay().getSize(screenSize); | |
Rect rect = new Rect(); | |
getContentView().getWindowVisibleDisplayFrame(rect); | |
int keyboardHeight = screenSize.y - rect.bottom; | |
log("onGlobalLayoutChanged | %d", keyboardHeight); | |
if (mOnKeyboardLayoutChangedListener != null) { | |
mOnKeyboardLayoutChangedListener.onKeyboardLayoutChanged(keyboardHeight); | |
} | |
} | |
interface OnKeyboardLayoutChangedListener { | |
void onKeyboardLayoutChanged(int height); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment