Skip to content

Commit 9c4ba8f

Browse files
pekingmedrchen
authored andcommitted
[Shape] Added state list size change for internal use; also updated StateListCornerSize to return default value similar to CSL.
PiperOrigin-RevId: 660030130
1 parent 703b884 commit 9c4ba8f

File tree

10 files changed

+454
-6
lines changed

10 files changed

+454
-6
lines changed

lib/java/com/google/android/material/shape/StateListCornerSize.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public CornerSize getCornerSizeForState(@NonNull int[] stateSet) {
140140
if (idx < 0) {
141141
idx = indexOfStateSet(StateSet.WILD_CARD);
142142
}
143-
return cornerSizes[idx];
143+
return idx < 0 ? defaultCornerSize : cornerSizes[idx];
144144
}
145145

146146
private int indexOfStateSet(int[] stateSet) {
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright 2024 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+
* https://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.google.android.material.shape;
18+
19+
import com.google.android.material.R;
20+
21+
import static android.content.res.Resources.ID_NULL;
22+
23+
import android.content.Context;
24+
import android.content.res.Resources;
25+
import android.content.res.Resources.Theme;
26+
import android.content.res.TypedArray;
27+
import android.content.res.XmlResourceParser;
28+
import android.util.AttributeSet;
29+
import android.util.StateSet;
30+
import android.util.TypedValue;
31+
import android.util.Xml;
32+
import androidx.annotation.NonNull;
33+
import androidx.annotation.Nullable;
34+
import androidx.annotation.Px;
35+
import androidx.annotation.RestrictTo;
36+
import androidx.annotation.RestrictTo.Scope;
37+
import androidx.annotation.StyleableRes;
38+
import java.io.IOException;
39+
import org.xmlpull.v1.XmlPullParser;
40+
import org.xmlpull.v1.XmlPullParserException;
41+
42+
/**
43+
* A state list of size values, which are used to define the change per state.
44+
*
45+
* @hide
46+
*/
47+
@RestrictTo(Scope.LIBRARY_GROUP)
48+
public class StateListSizeChange {
49+
private static final int INITIAL_CAPACITY = 10;
50+
int stateCount;
51+
@NonNull private SizeChange defaultSizeChange;
52+
@NonNull int[][] stateSpecs = new int[INITIAL_CAPACITY][];
53+
@NonNull SizeChange[] sizeChanges = new SizeChange[INITIAL_CAPACITY];
54+
55+
/**
56+
* Creates a {@link StateListSizeChange} from a styleable attribute.
57+
*
58+
* <p>If the attribute refers to an xml resource, the resource is parsed and the size values are
59+
* extracted from the items. If the attribute is not set or refers to a resource that cannot be
60+
* resolved, {@code null} will be returned.
61+
*
62+
* @param context the context
63+
* @param attributes the typed array in context
64+
* @param index the index of the styleable attribute
65+
* @return the {@link StateListSizeChange}
66+
*/
67+
@Nullable
68+
public static StateListSizeChange create(
69+
@NonNull Context context, @NonNull TypedArray attributes, @StyleableRes int index) {
70+
int resourceId = attributes.getResourceId(index, 0);
71+
if (resourceId == ID_NULL) {
72+
return null;
73+
}
74+
String typeName = context.getResources().getResourceTypeName(resourceId);
75+
if (!typeName.equals("xml")) {
76+
return null;
77+
}
78+
try (XmlResourceParser parser = context.getResources().getXml(resourceId)) {
79+
StateListSizeChange stateListSizeChange = new StateListSizeChange();
80+
AttributeSet attrs = Xml.asAttributeSet(parser);
81+
int type;
82+
83+
while ((type = parser.next()) != XmlPullParser.START_TAG
84+
&& type != XmlPullParser.END_DOCUMENT) {
85+
// Seek parser to start tag.
86+
}
87+
if (type != XmlPullParser.START_TAG) {
88+
throw new XmlPullParserException("No start tag found");
89+
}
90+
final String name = parser.getName();
91+
if (name.equals("selector")) {
92+
stateListSizeChange.loadSizeChangeFromItems(context, parser, attrs, context.getTheme());
93+
}
94+
return stateListSizeChange;
95+
} catch (XmlPullParserException | IOException | Resources.NotFoundException e) {
96+
return null;
97+
}
98+
}
99+
100+
public boolean isStateful() {
101+
return stateCount > 1;
102+
}
103+
104+
@NonNull
105+
public SizeChange getDefaultSizeChange() {
106+
return defaultSizeChange;
107+
}
108+
109+
@NonNull
110+
public SizeChange getSizeChangeForState(@NonNull int[] stateSet) {
111+
int idx = indexOfStateSet(stateSet);
112+
if (idx < 0) {
113+
idx = indexOfStateSet(StateSet.WILD_CARD);
114+
}
115+
return idx < 0 ? defaultSizeChange : sizeChanges[idx];
116+
}
117+
118+
private int indexOfStateSet(int[] stateSet) {
119+
final int[][] stateSpecs = this.stateSpecs;
120+
for (int i = 0; i < stateCount; i++) {
121+
if (StateSet.stateSetMatches(stateSpecs[i], stateSet)) {
122+
return i;
123+
}
124+
}
125+
return -1;
126+
}
127+
128+
private void loadSizeChangeFromItems(
129+
@NonNull Context context,
130+
@NonNull XmlPullParser parser,
131+
@NonNull AttributeSet attrs,
132+
@Nullable Theme theme)
133+
throws XmlPullParserException, IOException {
134+
final int innerDepth = parser.getDepth() + 1;
135+
int depth;
136+
int type;
137+
138+
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
139+
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
140+
if (type != XmlPullParser.START_TAG
141+
|| depth > innerDepth
142+
|| !parser.getName().equals("item")) {
143+
continue;
144+
}
145+
146+
Resources res = context.getResources();
147+
final TypedArray a =
148+
theme == null
149+
? res.obtainAttributes(attrs, R.styleable.StateListSizeChange)
150+
: theme.obtainStyledAttributes(attrs, R.styleable.StateListSizeChange, 0, 0);
151+
152+
SizeChangeAmount widthChangeAmount =
153+
getSizeChangeAmount(a, R.styleable.StateListSizeChange_widthChange, null);
154+
155+
a.recycle();
156+
157+
// Parse all unrecognized attributes as state specifiers.
158+
int j = 0;
159+
final int numAttrs = attrs.getAttributeCount();
160+
int[] stateSpec = new int[numAttrs];
161+
for (int i = 0; i < numAttrs; i++) {
162+
final int stateResId = attrs.getAttributeNameResource(i);
163+
if (stateResId != R.attr.widthChange) {
164+
stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) ? stateResId : -stateResId;
165+
}
166+
}
167+
stateSpec = StateSet.trimStateSet(stateSpec, j);
168+
addStateSizeChange(stateSpec, new SizeChange(widthChangeAmount));
169+
}
170+
}
171+
172+
@Nullable
173+
private SizeChangeAmount getSizeChangeAmount(
174+
@NonNull TypedArray a, int index, @Nullable SizeChangeAmount defaultValue) {
175+
TypedValue value = a.peekValue(index);
176+
if (value == null) {
177+
return defaultValue;
178+
}
179+
180+
if (value.type == TypedValue.TYPE_DIMENSION) {
181+
return new SizeChangeAmount(
182+
SizeChangeType.PIXELS,
183+
TypedValue.complexToDimensionPixelSize(value.data, a.getResources().getDisplayMetrics()));
184+
} else if (value.type == TypedValue.TYPE_FRACTION) {
185+
return new SizeChangeAmount(SizeChangeType.PERCENT, value.getFraction(1.0f, 1.0f));
186+
} else {
187+
return defaultValue;
188+
}
189+
}
190+
191+
private void addStateSizeChange(@NonNull int[] stateSpec, @NonNull SizeChange sizeChange) {
192+
if (stateCount == 0 || stateSpec.length == 0) {
193+
defaultSizeChange = sizeChange;
194+
}
195+
if (stateCount >= stateSpecs.length) {
196+
growArray(stateCount, stateCount + 10);
197+
}
198+
stateSpecs[stateCount] = stateSpec;
199+
sizeChanges[stateCount] = sizeChange;
200+
stateCount++;
201+
}
202+
203+
private void growArray(int oldSize, int newSize) {
204+
int[][] newStateSets = new int[newSize][];
205+
System.arraycopy(stateSpecs, 0, newStateSets, 0, oldSize);
206+
stateSpecs = newStateSets;
207+
SizeChange[] newSizeChanges = new SizeChange[newSize];
208+
System.arraycopy(sizeChanges, 0, newSizeChanges, 0, oldSize);
209+
sizeChanges = newSizeChanges;
210+
}
211+
212+
/** A collection of all values needed in a size change. */
213+
public static class SizeChange {
214+
SizeChangeAmount widthChange;
215+
216+
SizeChange(@Nullable SizeChangeAmount widthChange) {
217+
this.widthChange = widthChange;
218+
}
219+
}
220+
221+
/** The size change of one dimension, including the type and amount. */
222+
public static class SizeChangeAmount {
223+
SizeChangeType type;
224+
float amount;
225+
226+
SizeChangeAmount(SizeChangeType type, float amount) {
227+
this.type = type;
228+
this.amount = amount;
229+
}
230+
231+
public int getChange(@Px int baseSize) {
232+
if (type == SizeChangeType.PERCENT) {
233+
return (int) (amount * baseSize);
234+
}
235+
if (type == SizeChangeType.PIXELS) {
236+
return (int) amount;
237+
}
238+
return 0;
239+
}
240+
}
241+
242+
/** The type of size change. */
243+
public enum SizeChangeType {
244+
PERCENT,
245+
PIXELS
246+
}
247+
}

lib/java/com/google/android/material/shape/res/values/attrs.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@
6868
<enum name="rounded" value="0"/>
6969
<enum name="cut" value="1"/>
7070
</attr>
71+
72+
<declare-styleable name="StateListSizeChange">
73+
<!-- The change on width. Dimension for absolute value; fraction for relative value. -->
74+
<attr name="widthChange" format="dimension|fraction"/>
75+
</declare-styleable>
76+
7177
<!-- Shape appearance style reference with extra small corners. -->
7278
<attr name="shapeAppearanceCornerExtraSmall" format="reference"/>
7379
<!-- Shape appearance style reference with small corners. -->

lib/javatests/com/google/android/material/shape/StateListCornerSizeTest.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,50 @@ public void clearAttributeMap() {
5151
public void testCreateStateListWithStateList() {
5252
attributeMap.put(R.attr.testCornerSizeAttr, "@xml/state_list_corner_size");
5353
AttributeSet attributeSet = setupAttributeSetForTest();
54-
TypedArray attrs =
55-
context.obtainStyledAttributes(attributeSet, new int[] {R.attr.testCornerSizeAttr});
54+
TypedArray attrs = context.obtainStyledAttributes(attributeSet, R.styleable.ShapeTest);
5655
StateListCornerSize stateListCornerSize =
5756
StateListCornerSize.create(
5857
context, attrs, R.styleable.ShapeTest_testCornerSizeAttr, new AbsoluteCornerSize(0));
5958

6059
CornerSize pressedCornerSize =
6160
stateListCornerSize.getCornerSizeForState(new int[] {android.R.attr.state_pressed});
61+
CornerSize unspecifiedCornerSize =
62+
stateListCornerSize.getCornerSizeForState(new int[] {android.R.attr.state_hovered});
6263
CornerSize defaultCornerSize = stateListCornerSize.getDefaultCornerSize();
6364

6465
assertTrue(pressedCornerSize instanceof AbsoluteCornerSize);
6566
assertEquals(((AbsoluteCornerSize) pressedCornerSize).getCornerSize(), 2, FLOAT_TOLERANCE);
67+
assertTrue(unspecifiedCornerSize instanceof RelativeCornerSize);
68+
assertEquals(
69+
((RelativeCornerSize) unspecifiedCornerSize).getRelativePercent(), 0.5, FLOAT_TOLERANCE);
6670
assertTrue(defaultCornerSize instanceof RelativeCornerSize);
6771
assertEquals(
6872
((RelativeCornerSize) defaultCornerSize).getRelativePercent(), 0.5, FLOAT_TOLERANCE);
6973
}
7074

75+
@Test
76+
public void testCreateStateListWithStateListWithoutDefault() {
77+
attributeMap.put(R.attr.testCornerSizeAttr, "@xml/state_list_corner_size_without_default");
78+
AttributeSet attributeSet = setupAttributeSetForTest();
79+
TypedArray attrs = context.obtainStyledAttributes(attributeSet, R.styleable.ShapeTest);
80+
StateListCornerSize stateListCornerSize =
81+
StateListCornerSize.create(
82+
context, attrs, R.styleable.ShapeTest_testCornerSizeAttr, new AbsoluteCornerSize(0));
83+
84+
CornerSize pressedCornerSize =
85+
stateListCornerSize.getCornerSizeForState(new int[] {android.R.attr.state_pressed});
86+
CornerSize unspecifiedCornerSize =
87+
stateListCornerSize.getCornerSizeForState(new int[] {android.R.attr.state_hovered});
88+
CornerSize defaultCornerSize = stateListCornerSize.getDefaultCornerSize();
89+
90+
assertTrue(pressedCornerSize instanceof AbsoluteCornerSize);
91+
assertEquals(((AbsoluteCornerSize) pressedCornerSize).getCornerSize(), 2, FLOAT_TOLERANCE);
92+
assertTrue(unspecifiedCornerSize instanceof AbsoluteCornerSize);
93+
assertEquals(((AbsoluteCornerSize) unspecifiedCornerSize).getCornerSize(), 2, FLOAT_TOLERANCE);
94+
assertTrue(defaultCornerSize instanceof AbsoluteCornerSize);
95+
assertEquals(((AbsoluteCornerSize) defaultCornerSize).getCornerSize(), 2, FLOAT_TOLERANCE);
96+
}
97+
7198
@Test
7299
public void testCreateStateListWithDimensionValue() {
73100
attributeMap.put(R.attr.testCornerSizeAttr, "2dp");

0 commit comments

Comments
 (0)