• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher;
18 
19 import android.content.ActivityNotFoundException;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ResolveInfo;
24 import android.content.res.Configuration;
25 import android.graphics.drawable.Drawable;
26 import android.os.Bundle;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.KeyEvent;
30 import android.view.View;
31 import android.view.View.OnClickListener;
32 import android.view.View.OnKeyListener;
33 import android.view.View.OnLongClickListener;
34 import android.view.animation.AccelerateDecelerateInterpolator;
35 import android.view.animation.Animation;
36 import android.view.animation.Interpolator;
37 import android.view.animation.Transformation;
38 import android.view.inputmethod.InputMethodManager;
39 import android.widget.ImageButton;
40 import android.widget.LinearLayout;
41 import android.widget.TextView;
42 
43 public class Search extends LinearLayout
44         implements OnClickListener, OnKeyListener, OnLongClickListener {
45 
46     // Speed at which the widget slides up/down, in pixels/ms.
47     private static final float ANIMATION_VELOCITY = 1.0f;
48 
49     /** The distance in dips between the optical top of the widget and the top if its bounds */
50     private static final float WIDGET_TOP_OFFSET = 9;
51 
52     private final String TAG = "SearchWidget";
53 
54     private Launcher mLauncher;
55 
56     private TextView mSearchText;
57     private ImageButton mVoiceButton;
58 
59     /** The animation that morphs the search widget to the search dialog. */
60     private Animation mMorphAnimation;
61 
62     /** The animation that morphs the search widget back to its normal position. */
63     private Animation mUnmorphAnimation;
64 
65     // These four are passed to Launcher.startSearch() when the search widget
66     // has finished morphing. They are instance variables to make it possible to update
67     // them while the widget is morphing.
68     private String mInitialQuery;
69     private boolean mSelectInitialQuery;
70     private Bundle mAppSearchData;
71     private boolean mGlobalSearch;
72 
73     // For voice searching
74     private Intent mVoiceSearchIntent;
75 
76     private int mWidgetTopOffset;
77 
78     /**
79      * Used to inflate the Workspace from XML.
80      *
81      * @param context The application's context.
82      * @param attrs The attributes set containing the Workspace's customization values.
83      */
Search(Context context, AttributeSet attrs)84     public Search(Context context, AttributeSet attrs) {
85         super(context, attrs);
86 
87         final float scale = context.getResources().getDisplayMetrics().density;
88         mWidgetTopOffset = Math.round(WIDGET_TOP_OFFSET * scale);
89 
90         Interpolator interpolator = new AccelerateDecelerateInterpolator();
91 
92         mMorphAnimation = new ToParentOriginAnimation();
93         // no need to apply transformation before the animation starts,
94         // since the gadget is already in its normal place.
95         mMorphAnimation.setFillBefore(false);
96         // stay in the top position after the animation finishes
97         mMorphAnimation.setFillAfter(true);
98         mMorphAnimation.setInterpolator(interpolator);
99         mMorphAnimation.setAnimationListener(new Animation.AnimationListener() {
100             // The amount of time before the animation ends to show the search dialog.
101             private static final long TIME_BEFORE_ANIMATION_END = 80;
102 
103             // The runnable which we'll pass to our handler to show the search dialog.
104             private final Runnable mShowSearchDialogRunnable = new Runnable() {
105                 public void run() {
106                     showSearchDialog();
107                 }
108             };
109 
110             public void onAnimationEnd(Animation animation) { }
111             public void onAnimationRepeat(Animation animation) { }
112             public void onAnimationStart(Animation animation) {
113                 // Make the search dialog show up ideally *just* as the animation reaches
114                 // the top, to aid the illusion that the widget becomes the search dialog.
115                 // Otherwise, there is a short delay when the widget reaches the top before
116                 // the search dialog shows. We do this roughly 80ms before the animation ends.
117                 getHandler().postDelayed(
118                         mShowSearchDialogRunnable,
119                         Math.max(mMorphAnimation.getDuration() - TIME_BEFORE_ANIMATION_END, 0));
120             }
121         });
122 
123         mUnmorphAnimation = new FromParentOriginAnimation();
124         // stay in the top position until the animation starts
125         mUnmorphAnimation.setFillBefore(true);
126         // no need to apply transformation after the animation finishes,
127         // since the gadget is now back in its normal place.
128         mUnmorphAnimation.setFillAfter(false);
129         mUnmorphAnimation.setInterpolator(interpolator);
130         mUnmorphAnimation.setAnimationListener(new Animation.AnimationListener(){
131             public void onAnimationEnd(Animation animation) {
132                 clearAnimation();
133             }
134             public void onAnimationRepeat(Animation animation) { }
135             public void onAnimationStart(Animation animation) { }
136         });
137 
138         mVoiceSearchIntent = new Intent(android.speech.RecognizerIntent.ACTION_WEB_SEARCH);
139         mVoiceSearchIntent.putExtra(android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL,
140                 android.speech.RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
141     }
142 
143     /**
144      * Implements OnClickListener.
145      */
onClick(View v)146     public void onClick(View v) {
147         if (v == mVoiceButton) {
148             startVoiceSearch();
149         } else {
150             mLauncher.onSearchRequested();
151         }
152     }
153 
startVoiceSearch()154     private void startVoiceSearch() {
155         try {
156             getContext().startActivity(mVoiceSearchIntent);
157         } catch (ActivityNotFoundException ex) {
158             // Should not happen, since we check the availability of
159             // voice search before showing the button. But just in case...
160             Log.w(TAG, "Could not find voice search activity");
161         }
162     }
163 
164     /**
165      * Sets the query text. The query field is not editable, instead we forward
166      * the key events to the launcher, which keeps track of the text,
167      * calls setQuery() to show it, and gives it to the search dialog.
168      */
setQuery(String query)169     public void setQuery(String query) {
170         mSearchText.setText(query, TextView.BufferType.NORMAL);
171     }
172 
173     /**
174      * Morph the search gadget to the search dialog.
175      * See {@link Activity#startSearch()} for the arguments.
176      */
startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch)177     public void startSearch(String initialQuery, boolean selectInitialQuery,
178             Bundle appSearchData, boolean globalSearch) {
179         mInitialQuery = initialQuery;
180         mSelectInitialQuery = selectInitialQuery;
181         mAppSearchData = appSearchData;
182         mGlobalSearch = globalSearch;
183 
184         if (isAtTop()) {
185             showSearchDialog();
186         } else {
187             // Call up the keyboard before we actually call the search dialog so that it
188             // (hopefully) animates in at about the same time as the widget animation, and
189             // so that it becomes available as soon as possible. Only do this if a hard
190             // keyboard is not currently available.
191             if (getContext().getResources().getConfiguration().hardKeyboardHidden ==
192                     Configuration.HARDKEYBOARDHIDDEN_YES) {
193                 InputMethodManager inputManager = (InputMethodManager)
194                         getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
195                 inputManager.showSoftInputUnchecked(0, null);
196             }
197 
198             // Start the animation, unless it has already started.
199             if (getAnimation() != mMorphAnimation) {
200                 mMorphAnimation.setDuration(getAnimationDuration());
201                 startAnimation(mMorphAnimation);
202             }
203         }
204     }
205 
206     /**
207      * Shows the system search dialog immediately, without any animation.
208      */
showSearchDialog()209     private void showSearchDialog() {
210         mLauncher.showSearchDialog(
211                 mInitialQuery, mSelectInitialQuery, mAppSearchData, mGlobalSearch);
212     }
213 
214     /**
215      * Restore the search gadget to its normal position.
216      *
217      * @param animate Whether to animate the movement of the gadget.
218      */
stopSearch(boolean animate)219     public void stopSearch(boolean animate) {
220         setQuery("");
221 
222         // Only restore if we are not already restored.
223         if (getAnimation() == mMorphAnimation) {
224             if (animate && !isAtTop()) {
225                 mUnmorphAnimation.setDuration(getAnimationDuration());
226                 startAnimation(mUnmorphAnimation);
227             } else {
228                 clearAnimation();
229             }
230         }
231     }
232 
isAtTop()233     private boolean isAtTop() {
234         return getWidgetTop() == 0;
235     }
236 
getAnimationDuration()237     private int getAnimationDuration() {
238         return (int) (getWidgetTop() / ANIMATION_VELOCITY);
239     }
240 
241     /**
242      * Modify clearAnimation() to invalidate the parent. This works around
243      * an issue where the region where the end of the animation placed the view
244      * was not redrawn after clearing the animation.
245      */
246     @Override
clearAnimation()247     public void clearAnimation() {
248         Animation animation = getAnimation();
249         if (animation != null) {
250             super.clearAnimation();
251             if (animation.hasEnded()
252                     && animation.getFillAfter()
253                     && animation.willChangeBounds()) {
254                 ((View) getParent()).invalidate();
255             } else {
256                 invalidate();
257             }
258         }
259     }
260 
onKey(View v, int keyCode, KeyEvent event)261     public boolean onKey(View v, int keyCode, KeyEvent event) {
262         if (!event.isSystem() &&
263                 (keyCode != KeyEvent.KEYCODE_DPAD_UP) &&
264                 (keyCode != KeyEvent.KEYCODE_DPAD_DOWN) &&
265                 (keyCode != KeyEvent.KEYCODE_DPAD_LEFT) &&
266                 (keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) &&
267                 (keyCode != KeyEvent.KEYCODE_DPAD_CENTER)) {
268             // Forward key events to Launcher, which will forward text
269             // to search dialog
270             switch (event.getAction()) {
271                 case KeyEvent.ACTION_DOWN:
272                     return mLauncher.onKeyDown(keyCode, event);
273                 case KeyEvent.ACTION_MULTIPLE:
274                     return mLauncher.onKeyMultiple(keyCode, event.getRepeatCount(), event);
275                 case KeyEvent.ACTION_UP:
276                     return mLauncher.onKeyUp(keyCode, event);
277             }
278         }
279         return false;
280     }
281 
282     /**
283      * Implements OnLongClickListener to pass long clicks on child views
284      * to the widget. This makes it possible to pick up the widget by long
285      * clicking on the text field or a button.
286      */
onLongClick(View v)287     public boolean onLongClick(View v) {
288         return performLongClick();
289     }
290 
291     @Override
onFinishInflate()292     protected void onFinishInflate() {
293         super.onFinishInflate();
294 
295         mSearchText = (TextView) findViewById(R.id.search_src_text);
296         mVoiceButton = (ImageButton) findViewById(R.id.search_voice_btn);
297 
298         mSearchText.setOnKeyListener(this);
299 
300         mSearchText.setOnClickListener(this);
301         mVoiceButton.setOnClickListener(this);
302         setOnClickListener(this);
303 
304         mSearchText.setOnLongClickListener(this);
305         mVoiceButton.setOnLongClickListener(this);
306 
307         // Set the placeholder text to be the Google logo within the search widget.
308         Drawable googlePlaceholder =
309                 getContext().getResources().getDrawable(R.drawable.placeholder_google);
310         mSearchText.setCompoundDrawablesWithIntrinsicBounds(googlePlaceholder, null, null, null);
311 
312         configureVoiceSearchButton();
313     }
314 
315     @Override
onDetachedFromWindow()316     public void onDetachedFromWindow() {
317         super.onDetachedFromWindow();
318     }
319 
320     /**
321      * If appropriate & available, configure voice search
322      *
323      * Note:  Because the home screen search widget is always web search, we only check for
324      * getVoiceSearchLaunchWebSearch() modes.  We don't support the alternate form of app-specific
325      * voice search.
326      */
configureVoiceSearchButton()327     private void configureVoiceSearchButton() {
328         // Enable the voice search button if there is an activity that can handle it
329         PackageManager pm = getContext().getPackageManager();
330         ResolveInfo ri = pm.resolveActivity(mVoiceSearchIntent,
331                 PackageManager.MATCH_DEFAULT_ONLY);
332         boolean voiceSearchVisible = ri != null;
333 
334         // finally, set visible state of voice search button, as appropriate
335         mVoiceButton.setVisibility(voiceSearchVisible ? View.VISIBLE : View.GONE);
336     }
337 
338     /**
339      * Sets the {@link Launcher} that this gadget will call on to display the search dialog.
340      */
setLauncher(Launcher launcher)341     public void setLauncher(Launcher launcher) {
342         mLauncher = launcher;
343     }
344 
345     /**
346      * Moves the view to the top left corner of its parent.
347      */
348     private class ToParentOriginAnimation extends Animation {
349         @Override
applyTransformation(float interpolatedTime, Transformation t)350         protected void applyTransformation(float interpolatedTime, Transformation t) {
351             float dx = -getLeft() * interpolatedTime;
352             float dy = -getWidgetTop() * interpolatedTime;
353             t.getMatrix().setTranslate(dx, dy);
354         }
355     }
356 
357     /**
358      * Moves the view from the top left corner of its parent.
359      */
360     private class FromParentOriginAnimation extends Animation {
361         @Override
applyTransformation(float interpolatedTime, Transformation t)362         protected void applyTransformation(float interpolatedTime, Transformation t) {
363             float dx = -getLeft() * (1.0f - interpolatedTime);
364             float dy = -getWidgetTop() * (1.0f - interpolatedTime);
365             t.getMatrix().setTranslate(dx, dy);
366         }
367     }
368 
369     /**
370      * The widget is centered vertically within it's 4x1 slot. This is
371      * accomplished by nesting the actual widget inside another view. For
372      * animation purposes, we care about the top of the actual widget rather
373      * than it's container. This method return the top of the actual widget.
374      */
getWidgetTop()375     private int getWidgetTop() {
376         return getTop() + getChildAt(0).getTop() + mWidgetTopOffset;
377     }
378 
379 }
380