ConstraintLayoutStates.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 ConstraintLayoutStates {
    public static final String TAG = "ConstraintLayoutStates";
    private static final boolean DEBUG = false;
    private final ConstraintLayout mConstraintLayout;
    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;

    ConstraintLayoutStates(Context context, ConstraintLayout layout, int resourceID) {
        mConstraintLayout = layout;
        load(context, resourceID);
    }

    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 updateConstraints(int id, float width, float height) {
        if (mCurrentStateId == id) {
            State state;
            if (id == -1) {
                state = mStateList.valueAt(0); // id not being used take the first
            } else {
                state = mStateList.get(mCurrentStateId);

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

            ConstraintSet constraintSet = (match == -1) ? mDefaultConstraintSet :
                    state.mVariants.get(match).mConstraintSet;
            int cid = (match == -1) ? state.mConstraintID :
                    state.mVariants.get(match).mConstraintID;
            if (constraintSet == null) {
                return;
            }
            mCurrentConstraintNumber = match;
            if (mConstraintsChangedListener != null) {
                mConstraintsChangedListener.preLayoutChange(-1, cid);
            }
            constraintSet.applyTo(mConstraintLayout);
            if (mConstraintsChangedListener != null) {
                mConstraintsChangedListener.postLayoutChange(-1, cid);
            }

        } else {
            mCurrentStateId = id;
            State state = mStateList.get(mCurrentStateId);
            int match = state.findMatch(width, height);
            ConstraintSet constraintSet = (match == -1) ? state.mConstraintSet :
                    state.mVariants.get(match).mConstraintSet;
            int cid = (match == -1) ? state.mConstraintID :
                    state.mVariants.get(match).mConstraintID;

            if (constraintSet == null) {
                Log.v(TAG, "NO Constraint set found ! id=" + id + ", dim =" + width + ", " + height);
                return;
            }
            mCurrentConstraintNumber = match;
            if (mConstraintsChangedListener != null) {
                mConstraintsChangedListener.preLayoutChange(id, cid);
            }
            constraintSet.applyTo(mConstraintLayout);
            if (mConstraintsChangedListener != null) {
                mConstraintsChangedListener.postLayoutChange(id, cid);
            }
        }

    }

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

    /////////////////////////////////////////////////////////////////////////
    //      This represents one state
    /////////////////////////////////////////////////////////////////////////
    static class State {
        int mId;
        ArrayList<Variant> mVariants = new ArrayList<>();
        int mConstraintID = -1;
        ConstraintSet mConstraintSet;

        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)) {
                        mConstraintSet = new ConstraintSet();
                        mConstraintSet.clone(context, mConstraintID);
                        if (DEBUG) {
                            Log.v(TAG, "############### mConstraintSet.load(" + name + ")");
                        }
                    }
                }
            }
            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;
        ConstraintSet mConstraintSet;

        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)) {
                        mConstraintSet = new ConstraintSet();
                        if (DEBUG) {
                            Log.v(TAG, "############### mConstraintSet.load(" + name + ")");
                        }
                        mConstraintSet.clone(context, mConstraintID);
                        if (DEBUG) {
                            Log.v(TAG, "############### mConstraintSet.load(" + name + ")");
                        }
                    } else {
                        if (DEBUG) {
                            Log.v(TAG, "############### id -> ConstraintSet should be in this file");
                        }

                    }
                } 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;

        }
    }

    /**
     * Load a constraint set from a constraintSet.xml file
     *
     * @param context    the context for the inflation
     * @param resourceId mId of xml file in res/xml/
     */
    private void load(Context context, int resourceId) {
        if (DEBUG) {
            Log.v(TAG, "############### ");
        }
        Resources res = context.getResources();
        XmlPullParser parser = res.getXml(resourceId);
        String document = null;
        String tagName = null;
        try {
            Variant match;
            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;
                            case "ConstraintSet":
                                parseConstraintSet(context, parser);
                                break;
                            default:
                                if (DEBUG) {
                                    Log.v(TAG, "unknown tag " + tagName);
                                }
                        }

                        break;
                    case XmlResourceParser.END_TAG:
                        tagName = null;
                        break;
                    case XmlResourceParser.TEXT:
                        break;
                }
            }
//            for (Variant sizeMatch : mSizeMatchList) {
//                if (sizeMatch.mConstraintSet == null) {
//                    continue;
//                }
//                if (sizeMatch.mConstraintID != -1) {
//                    sizeMatch.mConstraintSet = mConstraintSetMap.get(sizeMatch.mConstraintID);
//                }
//            }
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void parseConstraintSet(Context context, XmlPullParser parser) {
        ConstraintSet set = new ConstraintSet();
        int count = parser.getAttributeCount();
        for (int i = 0; i < count; i++) {
            String name = parser.getAttributeName(i);
            String s = parser.getAttributeValue(i);
            if (name == null || s == null) continue;
            if ("id".equals(name)) {
                int id = -1;
                if (s.contains("/")) {
                    String tmp = s.substring(s.indexOf('/') + 1);
                    id = context.getResources().getIdentifier(tmp, "id", context.getPackageName());

                }
                if (id == -1) {
                    if (s.length() > 1) {
                        id = Integer.parseInt(s.substring(1));
                    } else {
                        Log.e(TAG, "error in parsing id");
                    }
                }
                set.load(context, parser);
                if (DEBUG) {
                    Log.v(TAG, " id name " + context.getResources().getResourceName(id));
                }
                mConstraintSetMap.put(id, set);
                break;
            }
        }
    }

}