package org.openintents.filemanager.view;
import java.io.File;
import org.openintents.filemanager.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView.ScaleType;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.ViewFlipper;
/**
* Provides a self contained way to represent the current path and provides a handy way of navigating. </br></br>
*
* <b>Note 1:</b> If you need to allow directory navigation outside of this class (e.g. when the user clicks on a folder from a {@link ListView}), use {@link #cd(File)} or {@link #cd(String)}. This is a requirement for the views of this class to
* properly refresh themselves. <i>You will get notified through the usual {@link OnDirectoryChangedListener}. </i></br>
*
* <b>Note 2:</b> To switch between {@link Mode Modes} use the {@link #switchToManualInput()} and {@link #switchToStandardInput()} methods!
*
* @author George Venios
*/
public class PathBar extends ViewFlipper {
private String TAG = this.getClass().getName();
/**
* The available Modes of this PathBar. </br> See {@link PathBar#switchToManualInput() switchToManualInput()} and {@link PathBar#switchToStandardInput() switchToStandardInput()}.
*/
public enum Mode {
/**
* The button path selection mode.
*/
STANDARD_INPUT,
/**
* The text path input mode.
*/
MANUAL_INPUT
}
private File mCurrentDirectory = null;
private Mode mCurrentMode = Mode.STANDARD_INPUT;
private File mInitialDirectory = null;
/** ImageButton used to switch to MANUAL_INPUT. */
private ImageButton mSwitchToManualModeButton = null;
/** Layout holding all path buttons. */
private PathButtonLayout mPathButtons = null;
/** Container of {@link #mPathButtons}. Allows horizontal scrolling. */
private HorizontalScrollView mPathButtonsContainer = null;
/** The EditText holding the path in MANUAL_INPUT. */
private EditText mPathEditText = null;
/** The ImageButton to confirm the manually entered path. */
private ImageButton mGoButton = null;
private OnDirectoryChangedListener mDirectoryChangedListener = new OnDirectoryChangedListener() {
@Override
public void directoryChanged(File newCurrentDir) {
}
};
public PathBar(Context context) {
super(context);
init();
}
public PathBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mCurrentDirectory = Environment.getExternalStorageDirectory();
mInitialDirectory = Environment.getExternalStorageDirectory();
this.setInAnimation(getContext(), R.anim.fade_in);
this.setOutAnimation(getContext(), R.anim.fade_out);
// RelativeLayout1
RelativeLayout standardModeLayout = new RelativeLayout(getContext());
{ // I use a block here so that layoutParams can be used as a variable name further down.
android.widget.ViewFlipper.LayoutParams layoutParams = new android.widget.ViewFlipper.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
standardModeLayout.setLayoutParams(layoutParams);
this.addView(standardModeLayout);
}
// ImageButton -- GONE. Kept this code in case we need to use an right-aligned button in the future.
mSwitchToManualModeButton = new ImageButton(getContext());
{
android.widget.RelativeLayout.LayoutParams layoutParams = new android.widget.RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mSwitchToManualModeButton.setLayoutParams(layoutParams);
mSwitchToManualModeButton.setId(R.id.path_bar_switch_to_manual_mode_button);
mSwitchToManualModeButton.setBackgroundDrawable(getItemBackground());
mSwitchToManualModeButton.setImageResource(R.drawable.ic_navbar_edit);
mSwitchToManualModeButton.setVisibility(View.GONE);
standardModeLayout.addView(mSwitchToManualModeButton);
}
// ImageButton -- GONE. Kept this code in case we need to use an left-aligned button in the future.
ImageButton cdToRootButton = new ImageButton(getContext());
{
android.widget.RelativeLayout.LayoutParams layoutParams = new android.widget.RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
cdToRootButton.setLayoutParams(layoutParams);
cdToRootButton.setId(R.id.path_bar_cd_to_root_button);
cdToRootButton.setBackgroundDrawable(getItemBackground());
cdToRootButton.setImageResource(R.drawable.ic_navbar_home);
cdToRootButton.setScaleType(ScaleType.CENTER_INSIDE);
cdToRootButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
cd("/");
}
});
cdToRootButton.setVisibility(View.GONE);
standardModeLayout.addView(cdToRootButton);
}
// Horizontal ScrollView container
mPathButtonsContainer = new HorizontalScrollView(getContext());
{
android.widget.RelativeLayout.LayoutParams layoutParams = new android.widget.RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.LEFT_OF,
mSwitchToManualModeButton.getId());
layoutParams.addRule(RelativeLayout.RIGHT_OF,
cdToRootButton.getId());
layoutParams.alignWithParent = true;
mPathButtonsContainer.setLayoutParams(layoutParams);
mPathButtonsContainer.setHorizontalScrollBarEnabled(false);
mPathButtonsContainer.setHorizontalFadingEdgeEnabled(true);
standardModeLayout.addView(mPathButtonsContainer);
}
// PathButtonLayout
mPathButtons = new PathButtonLayout(getContext());
{
android.widget.LinearLayout.LayoutParams layoutParams = new android.widget.LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
mPathButtons.setLayoutParams(layoutParams);
mPathButtons.setNavigationBar(this);
mPathButtonsContainer.addView(mPathButtons);
}
// RelativeLayout2
RelativeLayout manualModeLayout = new RelativeLayout(getContext());
{
android.widget.ViewFlipper.LayoutParams layoutParams = new android.widget.ViewFlipper.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
manualModeLayout.setLayoutParams(layoutParams);
this.addView(manualModeLayout);
}
// ImageButton
mGoButton = new ImageButton(getContext());
{
android.widget.RelativeLayout.LayoutParams layoutParams = new android.widget.RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mGoButton.setLayoutParams(layoutParams);
mGoButton.setId(R.id.path_bar_go_button);
mGoButton.setBackgroundDrawable(getItemBackground());
mGoButton.setImageResource(R.drawable.ic_navbar_accept);
mGoButton.setScaleType(ScaleType.CENTER_INSIDE);
mGoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
manualInputCd(mPathEditText.getText().toString());
}
});
manualModeLayout.addView(mGoButton);
}
// EditText
mPathEditText = new EditText(getContext());
{
android.widget.RelativeLayout.LayoutParams layoutParams = new android.widget.RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
layoutParams.alignWithParent = true;
layoutParams.addRule(RelativeLayout.LEFT_OF, mGoButton.getId());
mPathEditText.setLayoutParams(layoutParams);
mPathEditText.setInputType(InputType.TYPE_TEXT_VARIATION_URI);
mPathEditText.setImeOptions(EditorInfo.IME_ACTION_GO);
mPathEditText.setId(R.id.path_bar_path_edit_text);
mPathEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO
|| (event.getAction() == KeyEvent.ACTION_DOWN && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER || event.getKeyCode() == KeyEvent.KEYCODE_ENTER))) {
if (manualInputCd(v.getText().toString()))
// Since we have successfully navigated.
return true;
}
return false;
}
});
manualModeLayout.addView(mPathEditText);
}
}
/**
* Sets the directory the parent activity showed first so that back behavior is fixed.
*
* @param initDir
* The directory.
*/
public void setInitialDirectory(File initDir) {
mInitialDirectory = initDir;
cd(initDir);
}
/**
* See {@link #setInitialDirectory(File)}.
*/
public void setInitialDirectory(String initPath) {
setInitialDirectory(new File(initPath));
}
/**
* @see #setInitialDirectory(File)
* @return The initial directory.
*/
public File getInitialDirectory() {
return mInitialDirectory;
}
/**
* Get the currently active directory.
*
* @return A {@link File} representing the currently active directory.
*/
public File getCurrentDirectory() {
return mCurrentDirectory;
}
/**
* Use instead of {@link #cd(String)} when in {@link Mode#MANUAL_INPUT}.
*
* @param path The path to cd() to.
* @return true if the cd succeeded.
*/
boolean manualInputCd(String path) {
if (cd(path)) {
// if cd() successful, hide the keyboard
InputMethodManager imm = (InputMethodManager) getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindowToken(), 0);
switchToStandardInput();
return true;
} else {
Log.w(TAG, "Input path does not exist or is not a folder!");
return false;
}
}
/**
* {@code cd} to the passed file. If the file is legal input, sets it as the currently active Directory. Otherwise calls the listener to handle it, if any.
*
* @param file The file to {@code cd} to.
* @return Whether the path entered exists and can be navigated to.
*/
public boolean cd(File file) {
boolean res;
if (isFileOk(file)) {
// Set proper current directory.
mCurrentDirectory = file;
// Refresh button layout.
mPathButtons.refresh(mCurrentDirectory);
// Reset scrolling position. http://stackoverflow.com/questions/3263259/scrollview-scrollto-not-working-saving-scrollview-position-on-rotation
mPathButtonsContainer.post(new Runnable() {
@Override
public void run() {
mPathButtonsContainer.scrollTo(
mPathButtonsContainer.getMaxScrollAmount(),
(int) mPathButtonsContainer.getTop());
}
});
// Refresh manual input field.
mPathEditText.setText(file.getAbsolutePath());
res = true;
} else
res = false;
mDirectoryChangedListener.directoryChanged(file);
return res;
}
/**
* @see {@link org.openintents.filemanager.view.PathBar#cd(File) cd(File)}
* @param path
* The path of the Directory to {@code cd} to.
* @return Whether the path entered exists and can be navigated to.
*/
public boolean cd(String path) {
return cd(new File(path));
}
/**
* The same as running {@code File.listFiles()} on the currently active Directory.
*/
public File[] ls() {
return mCurrentDirectory.listFiles();
}
public void setOnDirectoryChangedListener(
OnDirectoryChangedListener listener) {
if (listener != null)
mDirectoryChangedListener = listener;
else
mDirectoryChangedListener = new OnDirectoryChangedListener() {
@Override
public void directoryChanged(File newCurrentDir) {
}
};
}
/**
* Switches to {@link Mode#MANUAL_INPUT}.
*/
public void switchToManualInput() {
setDisplayedChild(1);
mCurrentMode = Mode.MANUAL_INPUT;
}
/**
* Switches to {@link Mode#STANDARD_INPUT}.
*/
public void switchToStandardInput() {
setDisplayedChild(0);
mCurrentMode = Mode.STANDARD_INPUT;
}
/**
* Activities containing this bar, will have to call this method when the back button is pressed to provide correct backstack redirection and mode switching.
*
* @return Whether this view consumed the event.
*/
public boolean pressBack() {
// Switch mode.
if (mCurrentMode == Mode.MANUAL_INPUT) {
switchToStandardInput();
}
// Go back.
else if (mCurrentMode == Mode.STANDARD_INPUT) {
if (!backWillExit(mCurrentDirectory.getAbsolutePath())) {
cd(mCurrentDirectory.getParent());
return true;
} else
return false;
}
return true;
}
/**
* Returns the current {@link PathBar.Mode}.
*
*/
public Mode getMode() {
return mCurrentMode;
}
/**
*
* @param dirPath The current directory's absolute path.
* @return
*/
private boolean backWillExit(String dirPath) {
// Count tree depths
String[] dir = dirPath.split("/");
int dirTreeDepth = dir.length;
String[] init = mInitialDirectory.getAbsolutePath().split("/");
int initTreeDepth = init.length;
// analyze and return
if (dirTreeDepth > initTreeDepth) {
return false;
} else if (dirTreeDepth < initTreeDepth) {
return true;
} else {
return dirPath.equals(mInitialDirectory.getAbsolutePath());
}
}
@Override
public void setEnabled(boolean enabled) {
if(enabled)
switchToStandardInput();
else
switchToManualInput();
mPathEditText.setEnabled(enabled);
mGoButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
super.setEnabled(enabled);
}
/**
* Interface notifying users of this class when the user has chosen to navigate elsewhere.
*/
public interface OnDirectoryChangedListener {
public void directoryChanged(File newCurrentDir);
}
public PathButtonLayout getPathButtonLayout() {
return mPathButtons;
}
boolean isFileOk(File file) {
// Check file state.
boolean isFileOK = true;
isFileOK &= file.exists();
isFileOK &= file.isDirectory();
// add more filters here..
return isFileOK;
}
public Drawable getItemBackground(){
int[] attrs = new int[] {R.attr.pathBarItemBackground};
TypedArray ta = getContext().obtainStyledAttributes(attrs);
Drawable d = ta.getDrawable(0);
ta.recycle();
return d.mutate();
}
}