blob: 283042d19b037073ee942e5516c3b941c5bf0e81 [file] [log] [blame]
/*
* 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.launcher2;
import android.app.Activity;
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.graphics.drawable.Drawable;
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;
/** The distance in dips between the optical top of the widget and the top if its bounds */
private static final float WIDGET_TOP_OFFSET = 9;
private final String TAG = "Launcher.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;
private int mWidgetTopOffset;
/**
* 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);
final float scale = context.getResources().getDisplayMetrics().density;
mWidgetTopOffset = Math.round(WIDGET_TOP_OFFSET * scale);
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) {
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("");
// 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 getWidgetTop() == 0;
}
private int getAnimationDuration() {
return (int) (getWidgetTop() / 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 parent = (View) getParent();
if (parent != null) parent.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);
// Set the placeholder text to be the Google logo within the search widget.
Drawable googlePlaceholder =
getContext().getResources().getDrawable(R.drawable.placeholder_google);
mSearchText.setCompoundDrawablesWithIntrinsicBounds(googlePlaceholder, null, null, null);
configureVoiceSearchButton();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
/**
* 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 = -getWidgetTop() * 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 = -getWidgetTop() * (1.0f - interpolatedTime);
t.getMatrix().setTranslate(dx, dy);
}
}
/**
* The widget is centered vertically within it's 4x1 slot. This is accomplished by nesting
* the actual widget inside another view. For animation purposes, we care about the top of the
* actual widget rather than it's container. This method return the top of the actual widget.
*/
private int getWidgetTop() {
return getTop() + getChildAt(0).getTop() + mWidgetTopOffset;
}
}