StateSet.java

/*
 * Copyright (C) 2018 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 androidx.constraintlayout.widget;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;

/**
 * @hide
 */

public class StateSet {
    public static final String TAG = "ConstraintLayoutStates";
    private static final boolean DEBUG = false;
    int mDefaultState = -1;

     ConstraintSet mDefaultConstraintSet;
    int mCurrentStateId = -1; // default
    int mCurrentConstraintNumber = -1; // default
    private SparseArray<State> mStateList = new SparseArray<>();
    private SparseArray<ConstraintSet> mConstraintSetMap = new SparseArray<>();
    private ConstraintsChangedListener mConstraintsChangedListener = null;

    /**
     * Parse a StateSet
     * @param context
     * @param parser
     */
    public StateSet(Context context, XmlPullParser parser ) {
        load(context, parser);
    }

    /**
     * Load a constraint set from a constraintSet.xml file
     *
     * @param context    the context for the inflation
     * @param parser  mId of xml file in res/xml/
     */
    private void load(Context context,XmlPullParser parser) {
        if (DEBUG) {
            Log.v(TAG, "#########load stateSet###### ");
        }
        // Parse the stateSet attributes
        AttributeSet attrs = Xml.asAttributeSet(parser);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StateSet);
        final int N = a.getIndexCount();

        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            if (attr == R.styleable.StateSet_defaultState){
                mDefaultState = a.getResourceId(attr, mDefaultState);
            }
        }
        a.recycle();

        String tagName = null;
        try {
            Variant match;
            String document = null;
            State state = null;
            for (int eventType = parser.getEventType();
                 eventType != XmlResourceParser.END_DOCUMENT;
                 eventType = parser.next()) {

                switch (eventType) {
                    case XmlResourceParser.START_DOCUMENT:
                        document = parser.getName();
                        break;
                    case XmlResourceParser.START_TAG:
                        tagName = parser.getName();
                        switch(tagName) {
                            case "LayoutDescription":
                                break;
                            case "StateSet":
                                break;
                            case "State":
                                state = new State(context, parser);
                                mStateList.put(state.mId, state);
                                break;
                            case "Variant":
                                match = new Variant(context, parser);
                                if (state != null) {
                                    state.add(match);
                                }
                                break;

                            default:
                                if (DEBUG) {
                                    Log.v(TAG, "unknown tag "+tagName);
                                }
                        }

                        break;
                    case XmlResourceParser.END_TAG:
                        if ("StateSet".equals(parser.getName())) {
                            if (DEBUG) {
                                Log.v(TAG, "############ finished parsing state set");
                            }
                                return;
                        }

                        tagName = null;
                        break;
                    case XmlResourceParser.TEXT:
                        break;
                }
            }

        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean needsToChange(int id, float width, float height) {
        if (mCurrentStateId != id) {
            return true;
        }

        State state = (id == -1) ? mStateList.valueAt(0) : mStateList.get(mCurrentStateId);

        if (mCurrentConstraintNumber != -1) {
            if (state.mVariants.get(mCurrentConstraintNumber).match(width, height)) {
                return false;
            }
        }

        if (mCurrentConstraintNumber == state.findMatch(width, height)) {
            return false;
        }
        return true;
    }

    public void setOnConstraintsChanged(ConstraintsChangedListener constraintsChangedListener) {
        this.mConstraintsChangedListener = constraintsChangedListener;
    }

    public int stateGetConstraintID(int id, int width, int height) {
        return updateConstraints(-1, id, width, height);
    }

    /**
     * converts a state to a constraintSet
     *
     * @param currentConstrainSettId
     * @param stateId
     * @param width
     * @param height
     * @return
     */
    public int convertToConstraintSet(int currentConstrainSettId, int stateId, float width, float height) {
        State state = mStateList.get(stateId);
        if (state == null) {
            return stateId;
        }
        if (width == -1 || height == -1) {            // for the case without width/height matching
            if (state.mConstraintID == currentConstrainSettId) {
                return currentConstrainSettId;
            }
            for (Variant mVariant : state.mVariants) {
                if (currentConstrainSettId == mVariant.mConstraintID) {
                    return currentConstrainSettId;
                }
            }
            return state.mConstraintID;
        } else {
            Variant match = null;
            for (Variant mVariant : state.mVariants) {
                if (mVariant.match(width,height)) {
                    if (currentConstrainSettId == mVariant.mConstraintID) {
                        return currentConstrainSettId;
                    }
                    match = mVariant;
                }
            }
            if (match != null) {
                return match.mConstraintID;
            }

            return state.mConstraintID;
        }
    }

    public int updateConstraints(int currentId, int id, float width, float height) {
        if (currentId == id) {
            State state;
            if (id == -1) {
                state = mStateList.valueAt(0); // id not being used take the first
            } else {
                state = mStateList.get(mCurrentStateId);

            }
            if (state == null) {
                return -1;
            }
            if (mCurrentConstraintNumber != -1) {
                if (state.mVariants.get(currentId).match(width, height)) {
                    return currentId;
                }
            }
            int match = state.findMatch(width, height);
            if (currentId == match) {
                return currentId;
            }

            return (match == -1) ? state.mConstraintID : state.mVariants.get(match).mConstraintID;

        } else  {
            State state = mStateList.get(id);
            if (state == null) {
                return  -1;
            }
            int match = state.findMatch(width, height);
            return (match == -1) ? state.mConstraintID :  state.mVariants.get(match).mConstraintID;
        }

    }

    /////////////////////////////////////////////////////////////////////////
    //      This represents one state
    /////////////////////////////////////////////////////////////////////////
    static class State {
        int mId;
        ArrayList<Variant> mVariants = new ArrayList<>();
        int mConstraintID = -1;
        boolean mIsLayout = false;
        public State(Context context, XmlPullParser parser) {
            AttributeSet attrs = Xml.asAttributeSet(parser);
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.State);
            final int N = a.getIndexCount();
            for (int i = 0; i < N; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.State_android_id) {
                    mId = a.getResourceId(attr, mId);
                } else if (attr == R.styleable.State_constraints) {
                    mConstraintID = a.getResourceId(attr, mConstraintID);
                    String type = context.getResources().getResourceTypeName(mConstraintID);
                    String name = context.getResources().getResourceName(mConstraintID);

                    if ("layout".equals(type)) {
                        mIsLayout = true;
                    }
                }
            }
            a.recycle();
        }

        void add(Variant size) {
            mVariants.add(size);
        }

        public int findMatch(float width, float height) {
            for (int i = 0; i < mVariants.size(); i++) {
                if (mVariants.get(i).match(width, height)) {
                    return i;
                }
            }
            return -1;
        }
    }

    static class Variant {
        int mId;
        float mMinWidth = Float.NaN;
        float mMinHeight = Float.NaN;
        float mMaxWidth = Float.NaN;
        float mMaxHeight = Float.NaN;
        int mConstraintID = -1;
        boolean mIsLayout = false;

        public Variant(Context context, XmlPullParser parser) {
            AttributeSet attrs = Xml.asAttributeSet(parser);
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Variant);
            final int N = a.getIndexCount();
            if (DEBUG) {
                Log.v(TAG, "############### Variant");
            }

            for (int i = 0; i < N; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.Variant_constraints) {
                    mConstraintID = a.getResourceId(attr, mConstraintID);
                    String type = context.getResources().getResourceTypeName(mConstraintID);
                    String name = context.getResources().getResourceName(mConstraintID);

                    if ("layout".equals(type)) {
                        mIsLayout = true;
                    }
                } else if (attr == R.styleable.Variant_region_heightLessThan) {
                    mMaxHeight = a.getDimension(attr, mMaxHeight);
                } else if (attr == R.styleable.Variant_region_heightMoreThan) {
                    mMinHeight = a.getDimension(attr, mMinHeight);
                } else if (attr == R.styleable.Variant_region_widthLessThan) {
                    mMaxWidth = a.getDimension(attr, mMaxWidth);
                } else if (attr == R.styleable.Variant_region_widthMoreThan) {
                    mMinWidth = a.getDimension(attr, mMinWidth);
                } else {
                    Log.v(TAG, "Unknown tag");
                }
            }
            a.recycle();
            if (DEBUG) {
                Log.v(TAG, "############### Variant");
                if (!Float.isNaN(mMinWidth)) {
                    Log.v(TAG, "############### Variant mMinWidth " + mMinWidth);
                }
                if (!Float.isNaN(mMinHeight)) {
                    Log.v(TAG, "############### Variant mMinHeight " + mMinHeight);
                }
                if (!Float.isNaN(mMaxWidth)) {
                    Log.v(TAG, "############### Variant mMaxWidth " + mMaxWidth);
                }
                if (!Float.isNaN(mMaxHeight)) {
                    Log.v(TAG, "############### Variant mMinWidth " + mMaxHeight);
                }
            }
        }

        boolean match(float widthDp, float heightDp) {
            if (DEBUG) {
                Log.v(TAG, "width = " + (int) widthDp + " < " + mMinWidth + " && " + (int) widthDp + " > " + mMaxWidth +
                        " height = " + (int) heightDp + " < " + mMinHeight + " && " + (int) heightDp + " > " + mMaxHeight);
            }
            if (!Float.isNaN(mMinWidth)) {
                if (widthDp < mMinWidth) return false;
            }
            if (!Float.isNaN(mMinHeight)) {
                if (heightDp < mMinHeight) return false;
            }
            if (!Float.isNaN(mMaxWidth)) {
                if (widthDp > mMaxWidth) return false;
            }
            if (!Float.isNaN(mMaxHeight)) {
                if (heightDp > mMaxHeight) return false;
            }
            return true;

        }
    }

}