5

展開された状態と圧縮された状態を持つカスタム ビューの作成に取り組んでいます。圧縮された状態ではラベルとアイコンだけが表示され、展開された状態ではその下にメッセージが表示されます。これまでの動作のスクリーンショットを次に示します。

スクリーンショット

View、一度測定された圧縮状態と展開状態のサイズ値を保持するため、2 つの状態の間で簡単にアニメーション化できます。ビューを通常の方法で (たとえば a でLinearLayout) 使用すると、すべてが意図したとおりに機能します。ビュー サイズの変更は、次の呼び出しによって行われます。getLayoutParams().height = newHeight; requestLayout();

ただし、 で使用するとListView、ビューは再利用され、以前の高さが維持されます。そのため、ビューが非表示のときに展開されていた場合、次のリスト項目に再利用されるときに展開されたように表示されます。でレイアウトをリクエストしても、別のレイアウト パスを受信しないようですListAdapter。2 つの異なるビュー タイプ (展開と圧縮) を持つリサイクラーを使用することを検討しましたが、サイズはメッセージのサイズによって異なります。でビューが再アタッチされたときにリッスンできるイベントはありListViewますか? または、これを処理する方法について別の提案がありますか?

編集:これは、ビューの拡張および圧縮された高さを決定する方法です:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    if(r - l > 0 && b - t > 0 && dimensionsDirty) {
        int widthSpec = MeasureSpec.makeMeasureSpec(r - l, MeasureSpec.EXACTLY);
        messageView.setVisibility(GONE);
        measure(widthSpec, MeasureSpec.UNSPECIFIED);
        condensedHeight = getMeasuredHeight();

        messageView.setVisibility(VISIBLE);
        measure(widthSpec, MeasureSpec.UNSPECIFIED);
        expandedHeight = getMeasuredHeight();

        dimensionsDirty = false;
    }
}
4

2 に答える 2

7

編集:への両方の呼び出しのパラメーターの順序を修正しましたmakeMeasureSpec。奇妙なことに、それは私が持っていたのとは間違った方法で機能したので、私は冗長なことをしているのだろうかとほとんど思っています。いずれにせよ、それを指摘したかっただけです。以下でダウンロードするプロジェクトには、これらの修正はありません。

さて、これが理解できなかったのは本当に気になりました。そこで、レイアウトと測定システムに慣れることに決めました。これが私が思いついた解決策です。

  1. 単一の直接の子をホストするカスタムViewGroup拡張(のような)FrameLayoutScrollView
  2. ListAdapter各リストアイテムの展開/折りたたみ状態の追跡を処理するカスタム。
  3. OnItemClickListener折りたたみ状態と展開状態の間でアニメーション化するリクエストを処理する習慣。

ResizeLayoutスクリーンショット

他の誰かがそれが役に立つと思う場合に備えて、私はこのコードを共有したいと思います。かなり柔軟なはずですが、バグや改善の余地があることは間違いありません。1つは、プログラムでスクロールする際に問題が発生したためListView(ビューだけでなく、実際にコンテンツをスクロールする方法はないようです)smoothScrollToPosition(int)、ビューサイズを変更するたびに使用しました。これにはハードコードされた400msの期間がありますが、これは不要です。したがって、将来的には、期間が0(つまりscrollToPosition(int))の独自のバージョンを作成しようとする可能性があります。

一般的な使用法は次のとおりです。

  1. リストアイテムのXMLResizeLayoutには、階層のルートとしてが必要です。そこから、必要なレイアウト構造を構築できます。基本的には、通常のリストアイテムのレイアウトをResizeLayoutタグでラップするだけです。

  2. レイアウトでは、IDを持つ1つのビューが必要ですcollapse_to。これは、レイアウトが折り返されるビューです(つまり、どのビューが折りたたまれた高さを決定するか)。

  3. リストアダプタを介してリサイクルする場合の重要なこと:

    • reuse()リサイクルされたビューを取得するときは常に呼び出します(例convertView
    • setIsExpanded(boolean)リサイクルされたビューを返す前に、必ず電話してください。それ以外の場合は、リサイクル前の状態を保持します

最終的にこれをgitリポジトリにスローする可能性がありますが、今のところコードは次のとおりです。

ResizeLayout.java

これはコードの大部分です。また、テストに使用した自分Activityとそれをさらに下に含めます。Adapterそれらは非常に一般的ですが、効果的な使用法を示しています。

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.*;
import android.widget.FrameLayout;

/*
 * ResizeLayout
 * 
 * Custom ViewGroup that allows you to specify a view in the child hierarchy to wrap to, and 
 * allows for the view to be expanded to the full size of the content.
 * 
 * Author:  Kevin Coppock
 * Date:    2013/03/02
 */

public class ResizeLayout extends FrameLayout {
    private static final int PX_PER_SEC = 900; //Pixels per Second to animate layout changes
    private final LayoutAnimation animation = new LayoutAnimation();
    private final int wrapSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED);

    private int collapsedHeight = 0;
    private int expandedHeight = 0;
    private boolean contentsChanged = true;
    private State state = State.COLLAPSED;

    private OnLayoutChangedListener listener;

    public ResizeLayout(Context context) { super(context); }
    public ResizeLayout(Context context, AttributeSet attrs) { super(context, attrs); }
    public ResizeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if(getChildCount() > 0) {
            View child = getChildAt(0);
            child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
        }

        //If the layout parameters have changed and the view is animating, notify listeners
        if(changed && animation.isAnimating()) {
            switch(state) {
                case COLLAPSED: fireOnLayoutCollapsing(left, top, right, bottom); break;
                case EXPANDED:  fireOnLayoutExpanding(left, top, right, bottom); break;
            }
        }
    }

    /**
     * Reset the internal state of the view to defaults. This should be called any time you change the contents
     * of this ResizeLayout (e.g. recycling through a ListAdapter)
     */
    public void reuse() {
        collapsedHeight = expandedHeight = 0;
        contentsChanged = true;
        state = State.COLLAPSED;
        requestLayout();
    }

    /**
     * Set the state of the view. This should ONLY be called after a call to reuse() as it does not animate
     * the view; it simply sets the internal state. An example of usage is in a ListAdapter -- if the view is
     * recycled, it may be in the incorrect state, so it should be set here to the correct state before layout.
     * @param isExpanded whether or not the view should be in the expanded state
     */
    public void setIsExpanded(boolean isExpanded) {
        state = isExpanded ? State.EXPANDED : State.COLLAPSED;
    }

    /**
     * Animates the ResizeLayout between COLLAPSED and EXPANDED states, only if it is not currently animating.
     */
    public void animateToNextState() {
        if(!animation.isAnimating()) {
            animation.reuse(state.getStartHeight(this), state.getEndHeight(this));
            state = state.next();
            startAnimation(animation);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if(getChildCount() < 1) { //ResizeLayout has no child; default to spec, or padding if unspecified
            setMeasuredDimension(
                widthMode ==  MeasureSpec.UNSPECIFIED ? getPaddingLeft() + getPaddingRight() : width,
                heightMode == MeasureSpec.UNSPECIFIED ? getPaddingTop() + getPaddingBottom() : height
            );
            return;
        }

        View child = getChildAt(0); //Get the only child of the ResizeLayout

        if(contentsChanged) { //If the contents of the view have changed (first run, or after reset from reuse())
            contentsChanged = false;
            updateMeasurementsForChild(child, widthMeasureSpec, heightMeasureSpec);
            return;
        }

        //This state occurs on the second run. The child might be wrap_content, so the MeasureSpec will be unspecified.
        //Skip measuring the child and just accept the measurements from the first run.
        if(heightMode == MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(getWidth(), getHeight());
        } else {
            //Likely in mid-animation; we have a fixed-height from the MeasureSpec so use it
            child.measure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
        }
    }

    /**
     * Sets the measured dimension for this ResizeLayout, getting the initial measurements
     * for the condensed and expanded heights from the child view.
     * @param child the child view of this ResizeLayout
     * @param widthSpec the width MeasureSpec from onMeasure()
     * @param heightSpec the height MeasureSpec from onMeasure()
     */
    private void updateMeasurementsForChild(View child, int widthSpec, int heightSpec) {
        child.measure(widthSpec, wrapSpec); //Measure the child using WRAP_CONTENT for the height

        //Get the View that has been selected as the "collapse to" view (ID = R.id.collapse_to)
        View viewToCollapseTo = child.findViewById(R.id.collapse_to);

        if(viewToCollapseTo != null) {
            //The collapsed height should be the height of the collapseTo view + any top or bottom padding
            collapsedHeight = viewToCollapseTo.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();

            //The expanded height is simply the full height of the child (measured with WRAP_CONTENT)
            expandedHeight = child.getMeasuredHeight();

            //Re-Measure the child to reflect the state of the view (COLLAPSED or EXPANDED)
            int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(state.getStartHeight(this), MeasureSpec.EXACTLY);
            child.measure(widthSpec, newHeightMeasureSpec);
        }
        setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
    }

    @Override
    public void addView(View child) {
        if(getChildCount() > 0) {
            throw new IllegalArgumentException("ResizeLayout can host only one direct child.");
        } else {
            super.addView(child);
        }
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        if(getChildCount() > 0) {
            throw new IllegalArgumentException("ResizeLayout can host only one direct child.");
        } else {
            super.addView(child, index, params);
        }
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        if(getChildCount() > 0) {
            throw new IllegalArgumentException("ResizeLayout can host only one direct child.");
        } else {
            super.addView(child, params);
        }
    }

    @Override
    public void addView(View child, int width, int height) {
        if(getChildCount() > 0) {
            throw new IllegalArgumentException("ResizeLayout can host only one direct child.");
        } else {
            super.addView(child, width, height);
        }
    }

    /**
     * Handles animating the view between its expanded and collapsed states by adjusting the
     * layout parameters of the containing object and requesting a layout pass.
     */
    private class LayoutAnimation extends Animation implements Animation.AnimationListener {
        private int startHeight = 0, deltaHeight = 0;
        private boolean isAnimating = false;

        /**
         * Just a default interpolator and friction I think feels nice; can be changed.
         */
        public LayoutAnimation() {
            setInterpolator(new DecelerateInterpolator(2.2f));
            setAnimationListener(this);
        }

        /**
         * Sets the duration of the animation to a duration matching the specified value in
         * Pixels per Second (PPS). For example, if the view animation is 60 pixels, then a PPS of 60
         * would set a duration of 1000ms (i.e. duration = (delta / pps) * 1000). PPS is used rather
         * than a fixed time so that the animation speed is consistent regardless of the contents
         * of the view.
         * @param pps the number of pixels per second to resize the layout by
         */
        private void setDurationPixelsPerSecond(int pps) {
            setDuration((int) (((float) Math.abs(deltaHeight) / pps) * 1000));
        }

        /**
         * Allows reuse of a single LayoutAnimation object. Call this before starting the animation
         * to restart the animation and set the new parameters
         * @param startHeight the height from which the animation should begin
         * @param endHeight the height at which the animation should end
         */
        public void reuse(int startHeight, int endHeight) {
            reset();
            setStartTime(0);
            this.startHeight = startHeight;
            this.deltaHeight = endHeight - startHeight;
            setDurationPixelsPerSecond(PX_PER_SEC);
        }

        /**
         * Applies the height transformation to this containing ResizeLayout
         * @param interpolatedTime the time (0.0 - 1.0) interpolated based on the set interpolator
         * @param t the transformation associated with the animation -- not used here
         */
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            getLayoutParams().height = startHeight + (int)(deltaHeight * interpolatedTime);
            requestLayout();
        }

        public boolean isAnimating() {
            return isAnimating;
        }

        @Override
        public void onAnimationStart(Animation animation) {
            isAnimating = true;
        }

        @Override
        public void onAnimationEnd(Animation animation) {
            isAnimating = false;
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
            /*Not implemented*/
        }
    }

    /**
     * Interface to listen for layout changes during an animation
     */
    public interface OnLayoutChangedListener {
        public void onLayoutExpanding(int l, int t, int r, int b);
        public void onLayoutCollapsing(int l, int t, int r, int b);
    }

    /**
     * Sets a listener for changes to this view's layout
     * @param listener the listener for layout changes
     */
    public void setOnBoundsChangedListener(OnLayoutChangedListener listener) {
        this.listener = listener;
    }

    private void fireOnLayoutExpanding(int l, int t, int r, int b) {
        if(listener != null) listener.onLayoutExpanding(l, t, r, b);
    }

    private void fireOnLayoutCollapsing(int l, int t, int r, int b) {
        if(listener != null) listener.onLayoutCollapsing(l, t, r, b);
    }

    protected enum State {
        COLLAPSED{
            @Override
            public State next() {
                return EXPANDED;
            }

            @Override
            public int getEndHeight(ResizeLayout view) {
                return view.expandedHeight;
            }

            @Override
            public int getStartHeight(ResizeLayout view) {
                return view.collapsedHeight;
            }
        },
        EXPANDED{
            @Override
            public State next() {
                return COLLAPSED;
            }

            @Override
            public int getEndHeight(ResizeLayout view) {
                return view.collapsedHeight;
            }

            @Override
            public int getStartHeight(ResizeLayout view) {
                return view.expandedHeight;
            }
        };

        public abstract State next();
        public abstract int getStartHeight(ResizeLayout view);
        public abstract int getEndHeight(ResizeLayout view);
    }
}

MyActivity.java

ListActivityこの例の目的で使用した単純なものです。の子XMLを使用したmain.xmlジェネリックです。LinearLayoutListViewListActivity

import android.app.ListActivity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.TextView;

import java.util.HashSet;
import java.util.Set;

public class MyActivity extends ListActivity implements ResizeLayout.OnLayoutChangedListener, AdapterView.OnItemClickListener {
    private MyAdapter myAdapter;
    private int clickedItemPosition;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        myAdapter = new MyAdapter(this);
        setListAdapter(myAdapter);
        getListView().setOnItemClickListener(this);
        getListView().setSelector(new ColorDrawable(Color.TRANSPARENT));
    }

    @Override
    public void onLayoutExpanding(int l, int t, int r, int b) {
        //Keep the clicked view fully visible if it's expanding
        getListView().smoothScrollToPosition(clickedItemPosition);
    }

    @Override
    public void onLayoutCollapsing(int l, int t, int r, int b) {
        //Not handled currently
    }

    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
        clickedItemPosition = i;
        myAdapter.toggleExpandedState(i);
        ((ResizeLayout) view).animateToNextState();
    }

    private class MyAdapter extends BaseAdapter {
        private LayoutInflater inflater;
        private Set<Integer> expanded = new HashSet<Integer>();

        public MyAdapter(Context ctx) {
            inflater = LayoutInflater.from(ctx);
        }

        @Override
        public int getCount() {
            return 100;
        }

        @Override
        public Object getItem(int i) {
            return i + 1;
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        public void toggleExpandedState(int position) {
            if (expanded.contains(position)) {
                expanded.remove(position);
            } else {
                expanded.add(position);
            }
        }

        @Override
        public View getView(int i, View convertView, ViewGroup viewGroup) {
            ResizeLayout layout = (ResizeLayout) convertView;
            TextView title;

            //New instance; no view to recycle.
            if (layout == null) {
                layout = (ResizeLayout) inflater.inflate(R.layout.list_item, viewGroup, false);
                layout.setOnBoundsChangedListener(MyActivity.this);
                layout.setTag(layout.findViewById(R.id.title));
            }

            //Recycling a ResizeLayout; make sure to reset parameters with reuse()
            else layout.reuse();

            //Set the state of the View -- otherwise it will be in whatever state it was before recycling
            layout.setIsExpanded(expanded.contains(i));

            title = (TextView) layout.getTag();
            title.setText("List Item #" + i);

            return layout;
        }
    }
}

list_item.xml

基本的なリストアイテムのレイアウト例。上部にアイコンとタイトルがあり(アイコンはビューとして設定されていcollapse_toます)、メッセージビューが下に配置されています。

<?xml version="1.0" encoding="utf-8"?>
<com.example.resize.ResizeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <ImageView
            android:id="@+id/collapse_to"
            android:src="@drawable/holoku"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerInside"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:contentDescription="@string/icon_desc"
            tools:ignore="UseCompoundDrawables"
            />

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_alignTop="@id/collapse_to"
            android:layout_alignBottom="@id/collapse_to"
            android:layout_toRightOf="@id/collapse_to"
            android:gravity="center_vertical"
            android:paddingLeft="20dp"
            android:textSize="20dp"
            android:textColor="#198EBC"
            />

        <TextView
            android:id="@+id/text"
            android:layout_marginTop="10dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="12dp"
            android:textColor="#444444"
            android:layout_below="@id/collapse_to"
            android:text="@string/message"
            />
    </RelativeLayout>
</com.example.resize.ResizeLayout>

現在、API 17より前のバージョンではテストしていませんが、NewApiの問題に対してlintチェックを実行すると、2.2(API 8)までさかのぼって機能するはずです。

サンプルプロジェクトをダウンロードして自分で試してみたい場合は、ここからダウンロードできます

于 2013-03-03T03:51:55.747 に答える
0

アダプターの getView メソッドをオーバーライドして、convertView 変数を調べることができますか (これは、少なくとも私が見ている ArrayAdapter の 2 番目のパラメーターです)。その上で getLayoutParames を呼び出して高さを取得し、それに応じて位置変数に基づいて調整できるはずです。

于 2013-01-02T19:56:18.313 に答える