Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2015 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.setupwizardlib.view;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Canvas;
     23 import android.graphics.RectF;
     24 import android.os.Build;
     25 import android.util.AttributeSet;
     26 import android.view.LayoutInflater;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.view.WindowInsets;
     30 import android.view.accessibility.AccessibilityEvent;
     31 import android.widget.ListView;
     32 
     33 import com.android.setupwizardlib.R;
     34 
     35 /**
     36  * This class provides sticky header functionality in a list view, to use with
     37  * SetupWizardIllustration. To use this, add a header tagged with "sticky", or a header tagged with
     38  * "stickyContainer" and one of its child tagged as "sticky". The sticky container will be drawn
     39  * when the sticky element hits the top of the view.
     40  *
     41  * <p>There are a few things to note:
     42  * <ol>
     43  *   <li>The two supported scenarios are StickyHeaderListView -> Header (stickyContainer) -> sticky,
     44  *   and StickyHeaderListView -> Header (sticky). The arrow (->) represents parent/child
     45  *   relationship and must be immediate child.
     46  *   <li>The view does not work well with padding. b/16190933
     47  *   <li>If fitsSystemWindows is true, then this will offset the sticking position by the height of
     48  *   the system decorations at the top of the screen.
     49  * </ol>
     50  *
     51  * @see StickyHeaderScrollView
     52  */
     53 public class StickyHeaderListView extends ListView {
     54 
     55     private View mSticky;
     56     private View mStickyContainer;
     57     private int mStatusBarInset = 0;
     58     private RectF mStickyRect = new RectF();
     59 
     60     public StickyHeaderListView(Context context) {
     61         super(context);
     62         init(null, android.R.attr.listViewStyle);
     63     }
     64 
     65     public StickyHeaderListView(Context context, AttributeSet attrs) {
     66         super(context, attrs);
     67         init(attrs, android.R.attr.listViewStyle);
     68     }
     69 
     70     public StickyHeaderListView(Context context, AttributeSet attrs, int defStyleAttr) {
     71         super(context, attrs, defStyleAttr);
     72         init(attrs, defStyleAttr);
     73     }
     74 
     75     private void init(AttributeSet attrs, int defStyleAttr) {
     76         final TypedArray a = getContext().obtainStyledAttributes(attrs,
     77                 R.styleable.SuwStickyHeaderListView, defStyleAttr, 0);
     78         int headerResId = a.getResourceId(R.styleable.SuwStickyHeaderListView_suwHeader, 0);
     79         if (headerResId != 0) {
     80             LayoutInflater inflater = LayoutInflater.from(getContext());
     81             View header = inflater.inflate(headerResId, this, false);
     82             addHeaderView(header, null, false);
     83         }
     84         a.recycle();
     85     }
     86 
     87     @Override
     88     protected void onLayout(boolean changed, int l, int t, int r, int b) {
     89         super.onLayout(changed, l, t, r, b);
     90         if (mSticky == null) {
     91             updateStickyView();
     92         }
     93     }
     94 
     95     public void updateStickyView() {
     96         mSticky = findViewWithTag("sticky");
     97         mStickyContainer = findViewWithTag("stickyContainer");
     98     }
     99 
    100     @Override
    101     public boolean dispatchTouchEvent(MotionEvent ev) {
    102         if (mStickyRect.contains(ev.getX(), ev.getY())) {
    103             ev.offsetLocation(-mStickyRect.left, -mStickyRect.top);
    104             return mStickyContainer.dispatchTouchEvent(ev);
    105         } else {
    106             return super.dispatchTouchEvent(ev);
    107         }
    108     }
    109 
    110     @Override
    111     public void draw(Canvas canvas) {
    112         super.draw(canvas);
    113         if (mSticky != null) {
    114             final int saveCount = canvas.save();
    115             // The view to draw when sticking to the top
    116             final View drawTarget = mStickyContainer != null ? mStickyContainer : mSticky;
    117             // The offset to draw the view at when sticky
    118             final int drawOffset = mStickyContainer != null ? mSticky.getTop() : 0;
    119             // Position of the draw target, relative to the outside of the scrollView
    120             final int drawTop = drawTarget.getTop();
    121             if (drawTop + drawOffset < mStatusBarInset || !drawTarget.isShown()) {
    122                 // ListView does not translate the canvas, so we can simply draw at the top
    123                 mStickyRect.set(0, -drawOffset + mStatusBarInset, drawTarget.getWidth(),
    124                         drawTarget.getHeight() - drawOffset + mStatusBarInset);
    125                 canvas.translate(0, mStickyRect.top);
    126                 canvas.clipRect(0, 0, drawTarget.getWidth(), drawTarget.getHeight());
    127                 drawTarget.draw(canvas);
    128             } else {
    129                 mStickyRect.setEmpty();
    130             }
    131             canvas.restoreToCount(saveCount);
    132         }
    133     }
    134 
    135     @Override
    136     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    137     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    138         if (getFitsSystemWindows()) {
    139             mStatusBarInset = insets.getSystemWindowInsetTop();
    140             insets.replaceSystemWindowInsets(
    141                     insets.getSystemWindowInsetLeft(),
    142                     0, /* top */
    143                     insets.getSystemWindowInsetRight(),
    144                     insets.getSystemWindowInsetBottom()
    145             );
    146         }
    147         return insets;
    148     }
    149 
    150     @Override
    151     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    152         super.onInitializeAccessibilityEvent(event);
    153 
    154         // Decoration-only headers should not count as an item for accessibility, adjust the
    155         // accessibility event to account for that.
    156         final int numberOfHeaders = mSticky != null ? 1 : 0;
    157         event.setItemCount(event.getItemCount() - numberOfHeaders);
    158         event.setFromIndex(Math.max(event.getFromIndex() - numberOfHeaders, 0));
    159         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    160             event.setToIndex(Math.max(event.getToIndex() - numberOfHeaders, 0));
    161         }
    162     }
    163 }
    164