8283ccff7c
Forcing the soft keyboard open is done at a specific time when animating the search widget to the top of the screen in order to time the two animations well, but we were doing it anyway even if the search widget was already at the top of the screen and didn't need to animate. Okay, so git does kinda rock when you're on a plane. :)
359 lines
14 KiB
Java
359 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
*
|
|
* 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.android.launcher;
|
|
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.content.res.Configuration;
|
|
import android.os.Bundle;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.View;
|
|
import android.view.View.OnClickListener;
|
|
import android.view.View.OnKeyListener;
|
|
import android.view.View.OnLongClickListener;
|
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
import android.view.animation.Animation;
|
|
import android.view.animation.Interpolator;
|
|
import android.view.animation.Transformation;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.widget.ImageButton;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
public class Search extends LinearLayout
|
|
implements OnClickListener, OnKeyListener, OnLongClickListener {
|
|
|
|
// Speed at which the widget slides up/down, in pixels/ms.
|
|
private static final float ANIMATION_VELOCITY = 1.0f;
|
|
|
|
private final String TAG = "SearchWidget";
|
|
|
|
private Launcher mLauncher;
|
|
|
|
private TextView mSearchText;
|
|
private ImageButton mVoiceButton;
|
|
|
|
/** The animation that morphs the search widget to the search dialog. */
|
|
private Animation mMorphAnimation;
|
|
|
|
/** The animation that morphs the search widget back to its normal position. */
|
|
private Animation mUnmorphAnimation;
|
|
|
|
// These four are passed to Launcher.startSearch() when the search widget
|
|
// has finished morphing. They are instance variables to make it possible to update
|
|
// them while the widget is morphing.
|
|
private String mInitialQuery;
|
|
private boolean mSelectInitialQuery;
|
|
private Bundle mAppSearchData;
|
|
private boolean mGlobalSearch;
|
|
|
|
// For voice searching
|
|
private Intent mVoiceSearchIntent;
|
|
|
|
/**
|
|
* Used to inflate the Workspace from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attributes set containing the Workspace's customization values.
|
|
*/
|
|
public Search(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
Interpolator interpolator = new AccelerateDecelerateInterpolator();
|
|
|
|
mMorphAnimation = new ToParentOriginAnimation();
|
|
// no need to apply transformation before the animation starts,
|
|
// since the gadget is already in its normal place.
|
|
mMorphAnimation.setFillBefore(false);
|
|
// stay in the top position after the animation finishes
|
|
mMorphAnimation.setFillAfter(true);
|
|
mMorphAnimation.setInterpolator(interpolator);
|
|
mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
|
|
// The amount of time before the animation ends to show the search dialog.
|
|
private static final long TIME_BEFORE_ANIMATION_END = 80;
|
|
|
|
// The runnable which we'll pass to our handler to show the search dialog.
|
|
private final Runnable mShowSearchDialogRunnable = new Runnable() {
|
|
public void run() {
|
|
showSearchDialog();
|
|
}
|
|
};
|
|
|
|
public void onAnimationEnd(Animation animation) { }
|
|
public void onAnimationRepeat(Animation animation) { }
|
|
public void onAnimationStart(Animation animation) {
|
|
// Make the search dialog show up ideally *just* as the animation reaches
|
|
// the top, to aid the illusion that the widget becomes the search dialog.
|
|
// Otherwise, there is a short delay when the widget reaches the top before
|
|
// the search dialog shows. We do this roughly 80ms before the animation ends.
|
|
getHandler().postDelayed(
|
|
mShowSearchDialogRunnable,
|
|
Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
|
|
}
|
|
});
|
|
|
|
mUnmorphAnimation = new FromParentOriginAnimation();
|
|
// stay in the top position until the animation starts
|
|
mUnmorphAnimation.setFillBefore(true);
|
|
// no need to apply transformation after the animation finishes,
|
|
// since the gadget is now back in its normal place.
|
|
mUnmorphAnimation.setFillAfter(false);
|
|
mUnmorphAnimation.setInterpolator(interpolator);
|
|
mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
|
|
public void onAnimationEnd(Animation animation) {
|
|
clearAnimation();
|
|
}
|
|
public void onAnimationRepeat(Animation animation) { }
|
|
public void onAnimationStart(Animation animation) { }
|
|
});
|
|
|
|
mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
|
|
mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
|
android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
|
|
}
|
|
|
|
/**
|
|
* Implements OnClickListener.
|
|
*/
|
|
public void onClick(View v) {
|
|
if (v == mVoiceButton) {
|
|
startVoiceSearch();
|
|
} else {
|
|
mLauncher.onSearchRequested();
|
|
}
|
|
}
|
|
|
|
private void startVoiceSearch() {
|
|
try {
|
|
getContext().startActivity(mVoiceSearchIntent);
|
|
} catch (ActivityNotFoundException ex) {
|
|
// Should not happen, since we check the availability of
|
|
// voice search before showing the button. But just in case...
|
|
Log.w(TAG, "Could not find voice search activity");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the query text. The query field is not editable, instead we forward
|
|
* the key events to the launcher, which keeps track of the text,
|
|
* calls setQuery() to show it, and gives it to the search dialog.
|
|
*/
|
|
public void setQuery(String query) {
|
|
mSearchText.setText(query, TextView.BufferType.NORMAL);
|
|
}
|
|
|
|
/**
|
|
* Morph the search gadget to the search dialog.
|
|
* See {@link Activity.startSearch()} for the arguments.
|
|
*/
|
|
public void startSearch(String initialQuery, boolean selectInitialQuery,
|
|
Bundle appSearchData, boolean globalSearch) {
|
|
mInitialQuery = initialQuery;
|
|
mSelectInitialQuery = selectInitialQuery;
|
|
mAppSearchData = appSearchData;
|
|
mGlobalSearch = globalSearch;
|
|
|
|
if (isAtTop()) {
|
|
showSearchDialog();
|
|
} else {
|
|
// Call up the keyboard before we actually call the search dialog so that it
|
|
// (hopefully) animates in at about the same time as the widget animation, and
|
|
// so that it becomes available as soon as possible. Only do this if a hard
|
|
// keyboard is not currently available.
|
|
if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
|
|
Configuration.HARDKEYBOARDHIDDEN_YES) {
|
|
// Make sure the text field is not focusable, so it's not responsible for
|
|
// causing the whole view to shift up to accommodate the keyboard.
|
|
mSearchText.setFocusable(false);
|
|
|
|
InputMethodManager inputManager = (InputMethodManager)
|
|
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
inputManager.showSoftInputUnchecked(0, null);
|
|
}
|
|
|
|
// Start the animation, unless it has already started.
|
|
if (getAnimation() != mMorphAnimation) {
|
|
mMorphAnimation.setDuration(getAnimationDuration());
|
|
startAnimation(mMorphAnimation);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows the system search dialog immediately, without any animation.
|
|
*/
|
|
private void showSearchDialog() {
|
|
mLauncher.showSearchDialog(
|
|
mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
|
|
}
|
|
|
|
/**
|
|
* Restore the search gadget to its normal position.
|
|
*
|
|
* @param animate Whether to animate the movement of the gadget.
|
|
*/
|
|
public void stopSearch(boolean animate) {
|
|
setQuery("");
|
|
|
|
// Set the search field back to focusable after making it unfocusable in
|
|
// startSearch, so that the home screen doesn't try to shift around when the
|
|
// keyboard comes up.
|
|
mSearchText.setFocusable(true);
|
|
// Only restore if we are not already restored.
|
|
if (getAnimation() == mMorphAnimation) {
|
|
if (animate && !isAtTop()) {
|
|
mUnmorphAnimation.setDuration(getAnimationDuration());
|
|
startAnimation(mUnmorphAnimation);
|
|
} else {
|
|
clearAnimation();
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isAtTop() {
|
|
return getTop() == 0;
|
|
}
|
|
|
|
private int getAnimationDuration() {
|
|
return (int) (getTop() / ANIMATION_VELOCITY);
|
|
}
|
|
|
|
/**
|
|
* Modify clearAnimation() to invalidate the parent. This works around
|
|
* an issue where the region where the end of the animation placed the view
|
|
* was not redrawn after clearing the animation.
|
|
*/
|
|
@Override
|
|
public void clearAnimation() {
|
|
Animation animation = getAnimation();
|
|
if (animation != null) {
|
|
super.clearAnimation();
|
|
if (animation.hasEnded()
|
|
&& animation.getFillAfter()
|
|
&& animation.willChangeBounds()) {
|
|
((View) getParent()).invalidate();
|
|
} else {
|
|
invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
if (!event.isSystem() &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
|
|
(keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
|
|
// Forward key events to Launcher, which will forward text
|
|
// to search dialog
|
|
switch (event.getAction()) {
|
|
case KeyEvent.ACTION_DOWN:
|
|
return mLauncher.onKeyDown(keyCode, event);
|
|
case KeyEvent.ACTION_MULTIPLE:
|
|
return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
|
|
case KeyEvent.ACTION_UP:
|
|
return mLauncher.onKeyUp(keyCode, event);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Implements OnLongClickListener to pass long clicks on child views
|
|
* to the widget. This makes it possible to pick up the widget by long
|
|
* clicking on the text field or a button.
|
|
*/
|
|
public boolean onLongClick(View v) {
|
|
return performLongClick();
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
|
|
mSearchText = (TextView) findViewById(R.id.search_src_text);
|
|
mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
|
|
|
|
mSearchText.setOnKeyListener(this);
|
|
|
|
mSearchText.setOnClickListener(this);
|
|
mVoiceButton.setOnClickListener(this);
|
|
setOnClickListener(this);
|
|
|
|
mSearchText.setOnLongClickListener(this);
|
|
mVoiceButton.setOnLongClickListener(this);
|
|
|
|
configureVoiceSearchButton();
|
|
}
|
|
|
|
/**
|
|
* If appropriate & available, configure voice search
|
|
*
|
|
* Note: Because the home screen search widget is always web search, we only check for
|
|
* getVoiceSearchLaunchWebSearch() modes. We don't support the alternate form of app-specific
|
|
* voice search.
|
|
*/
|
|
private void configureVoiceSearchButton() {
|
|
// Enable the voice search button if there is an activity that can handle it
|
|
PackageManager pm = getContext().getPackageManager();
|
|
ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
|
|
PackageManager.MATCH_DEFAULT_ONLY);
|
|
boolean voiceSearchVisible = ri != null;
|
|
|
|
// finally, set visible state of voice search button, as appropriate
|
|
mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
|
|
}
|
|
|
|
/**
|
|
* Sets the {@link Launcher} that this gadget will call on to display the search dialog.
|
|
*/
|
|
public void setLauncher(Launcher launcher) {
|
|
mLauncher = launcher;
|
|
}
|
|
|
|
/**
|
|
* Moves the view to the top left corner of its parent.
|
|
*/
|
|
private class ToParentOriginAnimation extends Animation {
|
|
@Override
|
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
float dx = -getLeft() * interpolatedTime;
|
|
float dy = -getTop() * interpolatedTime;
|
|
t.getMatrix().setTranslate(dx, dy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the view from the top left corner of its parent.
|
|
*/
|
|
private class FromParentOriginAnimation extends Animation {
|
|
@Override
|
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
float dx = -getLeft() * (1.0f - interpolatedTime);
|
|
float dy = -getTop() * (1.0f - interpolatedTime);
|
|
t.getMatrix().setTranslate(dx, dy);
|
|
}
|
|
}
|
|
|
|
}
|