Home | History | Annotate | Download | only in recent
      1 /*
      2  * Copyright (C) 2011 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.systemui.recent;
     18 
     19 import android.animation.LayoutTransition;
     20 import android.content.Context;
     21 import android.content.res.Configuration;
     22 import android.database.DataSetObserver;
     23 import android.graphics.Canvas;
     24 import android.util.AttributeSet;
     25 import android.util.DisplayMetrics;
     26 import android.util.FloatMath;
     27 import android.util.Log;
     28 import android.view.MotionEvent;
     29 import android.view.View;
     30 import android.view.ViewConfiguration;
     31 import android.view.ViewTreeObserver;
     32 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     33 import android.widget.LinearLayout;
     34 import android.widget.ScrollView;
     35 
     36 import com.android.systemui.R;
     37 import com.android.systemui.SwipeHelper;
     38 import com.android.systemui.recent.RecentsPanelView.TaskDescriptionAdapter;
     39 
     40 import java.util.HashSet;
     41 import java.util.Iterator;
     42 
     43 public class RecentsVerticalScrollView extends ScrollView
     44         implements SwipeHelper.Callback, RecentsPanelView.RecentsScrollView {
     45     private static final String TAG = RecentsPanelView.TAG;
     46     private static final boolean DEBUG = RecentsPanelView.DEBUG;
     47     private LinearLayout mLinearLayout;
     48     private TaskDescriptionAdapter mAdapter;
     49     private RecentsCallback mCallback;
     50     protected int mLastScrollPosition;
     51     private SwipeHelper mSwipeHelper;
     52     private RecentsScrollViewPerformanceHelper mPerformanceHelper;
     53     private HashSet<View> mRecycledViews;
     54     private int mNumItemsInOneScreenful;
     55 
     56     public RecentsVerticalScrollView(Context context, AttributeSet attrs) {
     57         super(context, attrs, 0);
     58         float densityScale = getResources().getDisplayMetrics().density;
     59         float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
     60         mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
     61 
     62         mPerformanceHelper = RecentsScrollViewPerformanceHelper.create(context, attrs, this, true);
     63         mRecycledViews = new HashSet<View>();
     64     }
     65 
     66     public void setMinSwipeAlpha(float minAlpha) {
     67         mSwipeHelper.setMinAlpha(minAlpha);
     68     }
     69 
     70     private int scrollPositionOfMostRecent() {
     71         return mLinearLayout.getHeight() - getHeight();
     72     }
     73 
     74     private void addToRecycledViews(View v) {
     75         if (mRecycledViews.size() < mNumItemsInOneScreenful) {
     76             mRecycledViews.add(v);
     77         }
     78     }
     79 
     80     public View findViewForTask(int persistentTaskId) {
     81         for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
     82             View v = mLinearLayout.getChildAt(i);
     83             RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) v.getTag();
     84             if (holder.taskDescription.persistentTaskId == persistentTaskId) {
     85                 return v;
     86             }
     87         }
     88         return null;
     89     }
     90 
     91     private void update() {
     92         for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
     93             View v = mLinearLayout.getChildAt(i);
     94             addToRecycledViews(v);
     95             mAdapter.recycleView(v);
     96         }
     97         LayoutTransition transitioner = getLayoutTransition();
     98         setLayoutTransition(null);
     99 
    100         mLinearLayout.removeAllViews();
    101 
    102         // Once we can clear the data associated with individual item views,
    103         // we can get rid of the removeAllViews() and the code below will
    104         // recycle them.
    105         Iterator<View> recycledViews = mRecycledViews.iterator();
    106         for (int i = 0; i < mAdapter.getCount(); i++) {
    107             View old = null;
    108             if (recycledViews.hasNext()) {
    109                 old = recycledViews.next();
    110                 recycledViews.remove();
    111                 old.setVisibility(VISIBLE);
    112             }
    113             final View view = mAdapter.getView(i, old, mLinearLayout);
    114 
    115             if (mPerformanceHelper != null) {
    116                 mPerformanceHelper.addViewCallback(view);
    117             }
    118 
    119             OnTouchListener noOpListener = new OnTouchListener() {
    120                 @Override
    121                 public boolean onTouch(View v, MotionEvent event) {
    122                     return true;
    123                 }
    124             };
    125 
    126             view.setOnClickListener(new OnClickListener() {
    127                 public void onClick(View v) {
    128                     mCallback.dismiss();
    129                 }
    130             });
    131             // We don't want a click sound when we dimiss recents
    132             view.setSoundEffectsEnabled(false);
    133 
    134             OnClickListener launchAppListener = new OnClickListener() {
    135                 public void onClick(View v) {
    136                     mCallback.handleOnClick(view);
    137                 }
    138             };
    139 
    140             RecentsPanelView.ViewHolder holder = (RecentsPanelView.ViewHolder) view.getTag();
    141             final View thumbnailView = holder.thumbnailView;
    142             OnLongClickListener longClickListener = new OnLongClickListener() {
    143                 public boolean onLongClick(View v) {
    144                     final View anchorView = view.findViewById(R.id.app_description);
    145                     mCallback.handleLongPress(view, anchorView, thumbnailView);
    146                     return true;
    147                 }
    148             };
    149             thumbnailView.setClickable(true);
    150             thumbnailView.setOnClickListener(launchAppListener);
    151             thumbnailView.setOnLongClickListener(longClickListener);
    152 
    153             // We don't want to dismiss recents if a user clicks on the app title
    154             // (we also don't want to launch the app either, though, because the
    155             // app title is a small target and doesn't have great click feedback)
    156             final View appTitle = view.findViewById(R.id.app_label);
    157             appTitle.setContentDescription(" ");
    158             appTitle.setOnTouchListener(noOpListener);
    159             final View calloutLine = view.findViewById(R.id.recents_callout_line);
    160             if (calloutLine != null) {
    161                 calloutLine.setOnTouchListener(noOpListener);
    162             }
    163 
    164             mLinearLayout.addView(view);
    165         }
    166         setLayoutTransition(transitioner);
    167 
    168         // Scroll to end after initial layout.
    169         final OnGlobalLayoutListener updateScroll = new OnGlobalLayoutListener() {
    170                 public void onGlobalLayout() {
    171                     mLastScrollPosition = scrollPositionOfMostRecent();
    172                     scrollTo(0, mLastScrollPosition);
    173                     final ViewTreeObserver observer = getViewTreeObserver();
    174                     if (observer.isAlive()) {
    175                         observer.removeOnGlobalLayoutListener(this);
    176                     }
    177                 }
    178             };
    179         getViewTreeObserver().addOnGlobalLayoutListener(updateScroll);
    180     }
    181 
    182     @Override
    183     public void removeViewInLayout(final View view) {
    184         dismissChild(view);
    185     }
    186 
    187     public boolean onInterceptTouchEvent(MotionEvent ev) {
    188         if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()");
    189         return mSwipeHelper.onInterceptTouchEvent(ev) ||
    190             super.onInterceptTouchEvent(ev);
    191     }
    192 
    193     @Override
    194     public boolean onTouchEvent(MotionEvent ev) {
    195         return mSwipeHelper.onTouchEvent(ev) ||
    196             super.onTouchEvent(ev);
    197     }
    198 
    199     public boolean canChildBeDismissed(View v) {
    200         return true;
    201     }
    202 
    203     public void dismissChild(View v) {
    204         mSwipeHelper.dismissChild(v, 0);
    205     }
    206 
    207     public void onChildDismissed(View v) {
    208         addToRecycledViews(v);
    209         mLinearLayout.removeView(v);
    210         mCallback.handleSwipe(v);
    211         // Restore the alpha/translation parameters to what they were before swiping
    212         // (for when these items are recycled)
    213         View contentView = getChildContentView(v);
    214         contentView.setAlpha(1f);
    215         contentView.setTranslationX(0);
    216     }
    217 
    218     public void onBeginDrag(View v) {
    219         // We do this so the underlying ScrollView knows that it won't get
    220         // the chance to intercept events anymore
    221         requestDisallowInterceptTouchEvent(true);
    222     }
    223 
    224     public void onDragCancelled(View v) {
    225     }
    226 
    227     public View getChildAtPosition(MotionEvent ev) {
    228         final float x = ev.getX() + getScrollX();
    229         final float y = ev.getY() + getScrollY();
    230         for (int i = 0; i < mLinearLayout.getChildCount(); i++) {
    231             View item = mLinearLayout.getChildAt(i);
    232             if (item.getVisibility() == View.VISIBLE
    233                     && x >= item.getLeft() && x < item.getRight()
    234                     && y >= item.getTop() && y < item.getBottom()) {
    235                 return item;
    236             }
    237         }
    238         return null;
    239     }
    240 
    241     public View getChildContentView(View v) {
    242         return v.findViewById(R.id.recent_item);
    243     }
    244 
    245     @Override
    246     public void draw(Canvas canvas) {
    247         super.draw(canvas);
    248 
    249         if (mPerformanceHelper != null) {
    250             int paddingLeft = mPaddingLeft;
    251             final boolean offsetRequired = isPaddingOffsetRequired();
    252             if (offsetRequired) {
    253                 paddingLeft += getLeftPaddingOffset();
    254             }
    255 
    256             int left = mScrollX + paddingLeft;
    257             int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
    258             int top = mScrollY + getFadeTop(offsetRequired);
    259             int bottom = top + getFadeHeight(offsetRequired);
    260 
    261             if (offsetRequired) {
    262                 right += getRightPaddingOffset();
    263                 bottom += getBottomPaddingOffset();
    264             }
    265             mPerformanceHelper.drawCallback(canvas,
    266                     left, right, top, bottom, mScrollX, mScrollY,
    267                     getTopFadingEdgeStrength(), getBottomFadingEdgeStrength(),
    268                     0, 0);
    269         }
    270     }
    271 
    272     @Override
    273     public int getVerticalFadingEdgeLength() {
    274         if (mPerformanceHelper != null) {
    275             return mPerformanceHelper.getVerticalFadingEdgeLengthCallback();
    276         } else {
    277             return super.getVerticalFadingEdgeLength();
    278         }
    279     }
    280 
    281     @Override
    282     public int getHorizontalFadingEdgeLength() {
    283         if (mPerformanceHelper != null) {
    284             return mPerformanceHelper.getHorizontalFadingEdgeLengthCallback();
    285         } else {
    286             return super.getHorizontalFadingEdgeLength();
    287         }
    288     }
    289 
    290     @Override
    291     protected void onFinishInflate() {
    292         super.onFinishInflate();
    293         setScrollbarFadingEnabled(true);
    294         mLinearLayout = (LinearLayout) findViewById(R.id.recents_linear_layout);
    295         final int leftPadding = mContext.getResources()
    296             .getDimensionPixelOffset(R.dimen.status_bar_recents_thumbnail_left_margin);
    297         setOverScrollEffectPadding(leftPadding, 0);
    298     }
    299 
    300     @Override
    301     public void onAttachedToWindow() {
    302         if (mPerformanceHelper != null) {
    303             mPerformanceHelper.onAttachedToWindowCallback(
    304                     mCallback, mLinearLayout, isHardwareAccelerated());
    305         }
    306     }
    307 
    308     @Override
    309     protected void onConfigurationChanged(Configuration newConfig) {
    310         super.onConfigurationChanged(newConfig);
    311         float densityScale = getResources().getDisplayMetrics().density;
    312         mSwipeHelper.setDensityScale(densityScale);
    313         float pagingTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
    314         mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
    315     }
    316 
    317     private void setOverScrollEffectPadding(int leftPadding, int i) {
    318         // TODO Add to (Vertical)ScrollView
    319     }
    320 
    321     @Override
    322     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    323         super.onSizeChanged(w, h, oldw, oldh);
    324 
    325         // Skip this work if a transition is running; it sets the scroll values independently
    326         // and should not have those animated values clobbered by this logic
    327         LayoutTransition transition = mLinearLayout.getLayoutTransition();
    328         if (transition != null && transition.isRunning()) {
    329             return;
    330         }
    331         // Keep track of the last visible item in the list so we can restore it
    332         // to the bottom when the orientation changes.
    333         mLastScrollPosition = scrollPositionOfMostRecent();
    334 
    335         // This has to happen post-layout, so run it "in the future"
    336         post(new Runnable() {
    337             public void run() {
    338                 // Make sure we're still not clobbering the transition-set values, since this
    339                 // runnable launches asynchronously
    340                 LayoutTransition transition = mLinearLayout.getLayoutTransition();
    341                 if (transition == null || !transition.isRunning()) {
    342                     scrollTo(0, mLastScrollPosition);
    343                 }
    344             }
    345         });
    346     }
    347 
    348     @Override
    349     protected void onVisibilityChanged(View changedView, int visibility) {
    350         super.onVisibilityChanged(changedView, visibility);
    351         // scroll to bottom after reloading
    352         if (visibility == View.VISIBLE && changedView == this) {
    353             post(new Runnable() {
    354                 public void run() {
    355                     update();
    356                 }
    357             });
    358         }
    359     }
    360 
    361     public void setAdapter(TaskDescriptionAdapter adapter) {
    362         mAdapter = adapter;
    363         mAdapter.registerDataSetObserver(new DataSetObserver() {
    364             public void onChanged() {
    365                 update();
    366             }
    367 
    368             public void onInvalidated() {
    369                 update();
    370             }
    371         });
    372 
    373         DisplayMetrics dm = getResources().getDisplayMetrics();
    374         int childWidthMeasureSpec =
    375                 MeasureSpec.makeMeasureSpec(dm.widthPixels, MeasureSpec.AT_MOST);
    376         int childheightMeasureSpec =
    377                 MeasureSpec.makeMeasureSpec(dm.heightPixels, MeasureSpec.AT_MOST);
    378         View child = mAdapter.createView(mLinearLayout);
    379         child.measure(childWidthMeasureSpec, childheightMeasureSpec);
    380         mNumItemsInOneScreenful =
    381                 (int) FloatMath.ceil(dm.heightPixels / (float) child.getMeasuredHeight());
    382         addToRecycledViews(child);
    383 
    384         for (int i = 0; i < mNumItemsInOneScreenful - 1; i++) {
    385             addToRecycledViews(mAdapter.createView(mLinearLayout));
    386         }
    387     }
    388 
    389     public int numItemsInOneScreenful() {
    390         return mNumItemsInOneScreenful;
    391     }
    392 
    393     @Override
    394     public void setLayoutTransition(LayoutTransition transition) {
    395         // The layout transition applies to our embedded LinearLayout
    396         mLinearLayout.setLayoutTransition(transition);
    397     }
    398 
    399     public void setCallback(RecentsCallback callback) {
    400         mCallback = callback;
    401     }
    402 }
    403